open-lyric-plugin-player 0.1.6 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"mappings":"6aAkCA,SAAS,EAAe,EAAiC,CACvD,OAAO,MAAM,QAAQ,GAAU,KAAK,EAAI,EAAS,MAAM,OAAS,CAClE,CAYA,SAAS,EAAc,EAAiC,CACtD,IAAM,EAAQ,MAAM,QAAQ,GAAU,KAAK,EAAI,EAAS,MAAQ,CAAC,EAC3D,EAAkB,EAAM,OAAQ,GAAS,IAAS,CAAC,CAAC,CAAC,OACrD,EAAmB,EAAM,OAAQ,GAAS,EAAO,CAAC,CAAC,CAAC,OACpD,EAAe,EAAM,OAAQ,GAAS,EAAO,CAAC,EAC9C,EAAW,EAAa,OAC1B,KAAK,IAAI,GAAG,CAAY,EAAI,KAAK,IAAI,GAAG,CAAY,EACpD,EAEJ,OACE,KAAK,KAAK,EAAS,UAAY,GAAK,EAAG,CAAC,EAAI,IAC3C,MAAM,QAAQ,GAAU,MAAM,EAAI,EAAS,OAAO,OAAS,GAAK,EACjE,EAAW,EACX,EAAa,OACb,EAAmB,GACnB,EAAkB,GAEtB,CAsCA,SAAS,EAAa,EAAiC,CACrD,IAAM,EAAQ,MAAM,QAAQ,GAAU,KAAK,EAAI,EAAS,MAAQ,CAAC,EAC3D,EAAU,MAAM,QAAQ,GAAU,OAAO,EAAI,EAAS,QAAU,CAAC,EACjE,EAAS,MAAM,QAAQ,GAAU,MAAM,EAAI,EAAS,OAAS,CAAC,EAC9D,EAAc,EAAe,CAAQ,EAE3C,OAAO,EACJ,IAAK,GAAc,CAClB,IAAM,EAAkB,EACrB,KAAK,EAAM,KAAW,CACrB,OACA,QACA,OAAQ,EAAQ,IAAU,CAC5B,EAAE,CAAC,CACF,OAAQ,GAAU,EAAM,OAAS,CAAS,EAE7C,GAAI,CAAC,EAAgB,OACnB,OAAO,KAGT,IAAM,EACJ,EAAgB,KAAM,GAAU,EAAM,OAAS,CAAC,CAAC,EAAE,QAAU,EACzD,EAAkB,EACpB,EAAgB,OAAQ,GAAU,EAAM,SAAW,CAAY,EAC/D,EACE,EAAY,EAAgB,EAAE,EAAE,MAChC,EAAU,EAAgB,EAAgB,OAAS,EAAE,EAAE,MAE7D,GACE,OAAO,GAAc,UACrB,OAAO,GAAY,UACnB,IAAc,EAEd,OAAO,KAGT,IAAM,EAAc,EAAe,EAAI,OAAO,CAAY,EAAI,OAE9D,MAAO,CACL,WAAY,EAAc,EAC1B,SAAU,EAAc,EACxB,KAAM,EACN,MAAO,MACP,KAAM,CACR,CACF,CAAC,CAAC,CACD,OAAQ,GAAU,IAAU,IAAI,CACrC,CAGA,SAAS,EAAmB,EAAiC,CAC3D,IAAM,EAA6C,CAAC,EAC9C,EAAc,EAAe,CAAQ,EAE3C,IAAK,GAAM,CAAC,EAAO,KAAU,GAAU,OAAS,CAAC,GAAG,QAAQ,EAAG,CAC7D,IAAM,EAAe,EAAc,EAC7B,EAAe,GAAU,UAAU,IAAU,EAEnD,GAAI,EAAO,EAAG,CACZ,EAAQ,KAAK,CAAC,EAAc,GAAG,CAAC,EAChC,QACF,CAEA,GAAI,IAAS,EAAG,CACd,EAAQ,KAAK,CAAC,EAAc,CAAC,CAAC,EAC9B,QACF,CAEA,EAAQ,KAAK,CACX,EACA,EACA,EAAe,EAAI,OAAO,CAAY,EAAI,MAC5C,CAAC,CACH,CAEA,MAAO,CACL,UACA,SAAU,KAAK,IAAI,GAAU,UAAY,EAAG,CAAC,EAC7C,OAAQ,EAAa,CAAQ,CAC/B,CACF,+zCC7Ia,EAAmB,CAC9B,OAAQ,CAAC,GAAI,GAAI,GAAI,GAAI,GAAI,EAAE,EAC/B,QAAS,CAAC,GAAI,GAAI,GAAI,EAAE,EACxB,mBAAoB,CAAC,GAAI,GAAI,GAAI,EAAE,EACnC,KAAM,CAAC,GAAI,GAAI,GAAI,EAAE,CACvB,EAYA,SAAgB,EACd,EACqB,CACrB,OAAQ,EAAR,CACE,IAAK,mBACL,IAAK,kBACH,MAAO,SACT,IAAK,iBACL,IAAK,QACH,MAAO,QACT,IAAK,eACH,MAAO,OACT,QACE,OAAO,CACX,CACF,CAgEA,IAAM,EAAyB,GACzB,EAAW,GACX,GAAY,EACZ,EAAc,EACd,EAAoB,IAMpB,GAAsB,EAM5B,SAAgB,IAAiD,CAI/D,OAAO,EAAU,IAAK,IAAW,CAC/B,KAAM,EAAM,KACZ,OAAQ,EAAM,KAAK,QAAS,GAC1B,EAAI,MAAM,GAAG,CAAC,CAAC,IAAK,GAAa,EAAS,KAAK,CAAC,CAClD,CACF,EAAE,CACJ,CAEA,SAAS,EAAM,EAAe,CAC5B,OAAS,EAAQ,GAAM,IAAM,EAC/B,CAMA,SAAS,EAAmB,EAAc,EAAgB,CACxD,IAAM,EAAW,EAAY,CAAI,EAMjC,OAJI,EAAS,MACJ,GAIN,EAAS,SAAW,GAAK,EAAS,IAAM,KACxC,CAAC,GAAS,CAAC,EAAG,GAAI,EAAE,CAAC,CAAC,SAAS,EAAS,GAAG,CAEhD,CAGA,SAAgB,GAAW,EAAqC,CAC9D,IAAM,EAAO,OAAO,GAAS,EAAE,CAAC,CAAC,KAAK,EAChC,CAAC,EAAM,GAAQ,EAAK,MAAM,GAAG,EAC7B,EAAQ,EAAc,CAAI,EAC1B,EAAa,EAAc,EAAM,OAAS,EAAE,EAElD,GAAI,EAAM,OAAS,CAAC,EAAM,OAAS,CAAC,OAAO,SAAS,CAAU,EAC5D,OAAO,KAGT,IAAM,EAAa,EAAO,EAAc,CAAI,EAAI,EAEhD,GAAI,CAAC,OAAO,SAAS,CAAU,EAC7B,OAAO,KAGT,IAAM,EAAY,EAAM,UAAU,IAC/B,GAAa,EAAY,CAAQ,CAAC,CAAC,WAAa,CACnD,EACM,EAAkB,EAAU,IAAK,GAAS,EAAM,EAAa,CAAI,CAAC,EAClE,EAAW,EAAM,UACpB,QACE,EAAU,IACT,CAAC,EAAmB,EAAU,IAAU,EAAM,UAAU,OAAS,CAAC,CACtE,CAAC,CACA,IAAK,GACJ,EAAM,GAAc,EAAY,CAAQ,CAAC,CAAC,WAAa,EAAE,CAC3D,EAEF,MAAO,CACL,OACA,aACA,aACA,QAAS,CAAC,GAAG,IAAI,IAAI,CAAC,EAAY,GAAG,CAAe,CAAC,CAAC,EACtD,gBAAiB,CAAC,GAAG,IAAI,IAAI,CAAC,EAAY,GAAG,CAAQ,CAAC,CAAC,EACvD,WACF,CACF,CAOA,SAAgB,GACd,EACA,EACA,CAAE,cAAc,IAAoC,CAAC,EAC5C,CACT,GAAI,CAAC,EAAK,OACR,MAAO,GAGT,IAAM,EAAW,IAAI,IAAI,EAAK,IAAI,CAAK,CAAC,EAExC,MACE,CAAC,GAAG,CAAQ,CAAC,CAAC,MAAO,GAAW,EAAS,QAAQ,SAAS,CAAM,CAAC,GACjE,EAAS,gBAAgB,MAAO,GAAW,EAAS,IAAI,CAAM,CAAC,IAC9D,CAAC,GAAe,EAAM,KAAK,IAAI,GAAG,CAAI,CAAC,IAAM,EAAS,WAE3D,CAOA,SAAS,GAAa,EAAiC,CACrD,IAAM,EAAQ,EAAS,MAAM,OAAQ,GAAS,EAAO,CAAC,CAAC,CAAC,OAExD,OAAO,EAAc,CAAQ,EAAI,EAAQ,CAC3C,CAGA,SAAgB,EACd,EACA,EACA,EACU,CACV,IAAM,EAAS,KAAK,IAAI,EAAU,CAAC,EAAI,EAEvC,OAAO,EAAM,SAAS,EAAM,IAC1B,EAAO,EAAI,CAAC,EAAI,CAAC,EAAO,IAAU,IAAS,EAAI,EAAI,EAAO,EAAO,CACnE,CACF,CAEA,SAAU,GACR,EACA,EAAQ,EACR,EAAmB,CAAC,EACC,CACrB,GAAI,IAAU,EAAQ,OAAQ,CAC5B,KAAM,CAAC,GAAG,CAAM,EAChB,MACF,CAEA,IAAK,IAAM,KAAU,EAAQ,GAC3B,EAAO,KAAK,CAAM,EAClB,MAAO,GAAQ,EAAS,EAAQ,EAAG,CAAM,EACzC,EAAO,IAAI,CAEf,CAcA,SAAgB,GACd,EACA,EACA,CACE,cAAc,GACd,WAAW,KAAK,IAAI,EAAG,EAAO,MAAM,GACY,CAAC,EAC5B,CACvB,IAAI,EAAkD,KAEtD,IAAK,IAAI,EAAM,EAAG,EAAM,IAAa,EAAU,GAAO,EAAG,CACvD,IAAM,EAAS,CACb,EACA,GAAG,MAAM,KAAK,CAAE,OAAQ,CAAc,GAAI,EAAG,IAAW,EAAM,CAAM,CACtE,EACM,EAAU,EAAO,IAAK,GAAS,CACnC,GACA,GAAG,EAAO,OAAQ,GAAS,EAAS,QAAQ,SAAS,EAAM,EAAO,CAAI,CAAC,CAAC,CAC1E,CAAC,EAED,IAAK,IAAM,KAAS,GAAQ,CAAO,EAAG,CACpC,IAAM,EAAQ,EAAM,UAAW,GAAS,GAAQ,CAAC,EAC3C,EAAO,EAAM,cAAe,GAAS,GAAQ,CAAC,EAC9C,EAAW,EAAO,EAAQ,EAEhC,GACE,IAAU,IACV,EAAW,GACX,EAAM,MAAM,EAAO,EAAO,CAAC,CAAC,CAAC,SAAS,EAAE,EAExC,SAGF,IAAM,EAAU,EAAM,OAAQ,GAAS,EAAO,CAAC,EACzC,EAAa,KAAK,IAAI,GAAG,CAAO,EAMtC,IAJE,EAAQ,OAAS,EACb,EAAQ,OAAQ,GAAS,IAAS,CAAU,CAAC,CAAC,OAAS,EACvD,EAAQ,QAGF,GACV,CAAC,GAAY,EAAgB,EAAQ,EAAO,CAAC,EAAG,EAAU,CACxD,aACF,CAAC,EAED,SAGF,IAAM,EAAQ,GAAa,EAAW,CAAK,CAAC,GAExC,CAAC,GAAQ,EAAQ,EAAK,SACxB,EAAO,CAAE,QAAO,OAAM,EAE1B,CACF,CAEA,GAAI,CAAC,EACH,OAAO,KAGT,IAAM,EAAW,EAAW,EAAK,KAAK,EAEtC,MAAO,CACL,MAAO,EAAS,MAChB,SAAU,EAAS,SACnB,KAAM,EAAgB,EAAQ,EAAK,MAAO,CAAC,EAC3C,OAAQ,EAAS,QAAU,CAAC,EAC5B,OAAQ,UACV,CACF,CAGA,SAAS,EAAW,EAAiD,CACnE,IAAM,EAAU,EAAM,OAAQ,GAAS,EAAO,CAAC,EACzC,EAAU,EAAQ,OAAS,KAAK,IAAI,GAAG,CAAO,EAAI,EAClD,EAAS,EAAQ,OAAS,KAAK,IAAI,GAAG,CAAO,EAAI,EACjD,EAAW,GAAW,EAAI,EAAI,EAC9B,EAAW,EAAM,IAAK,GAAU,EAAO,EAAI,EAAO,EAAW,EAAI,CAAK,EACtE,EAAkB,EAAS,OAAQ,GAAS,EAAO,CAAC,EAE1D,MAAO,CACL,MAAO,EACP,WACA,OACE,EAAgB,OAAS,EACrB,CAAC,KAAK,IAAI,GAAG,CAAe,CAAC,EAC7B,CAAC,CACT,CACF,CAGA,SAAS,GAAgB,EAAmC,CAC1D,OAAO,EAAQ,MAAM,IAAK,GACxB,EAAO,EAAI,EAAO,EAAQ,SAAW,EAAI,CAC3C,CACF,CAGA,SAAS,GAAgB,EAAyB,EAAgB,CAChE,OAAO,OAAO,KAAK,EAAS,MAAM,CAAC,CAAC,KACjC,GAAS,EAAc,EAAK,QAAQ,QAAS,GAAG,CAAC,IAAM,CAC1D,CACF,CAGA,SAAgB,GACd,EACA,EACA,EACA,CAAE,cAAc,IAAoC,CAAC,EACR,CAC7C,IAAM,EAAO,GAAgB,EAAU,EAAS,UAAU,EACtD,EAAoD,KAExD,IAAK,IAAM,KAAS,EAAO,EAAS,OAAO,GAAQ,CAAC,EAClD,IAAK,IAAM,KAAY,EAAM,UAAW,CACtC,IAAM,EAAW,EAAS,UAAY,EAChC,EAAO,EAAgB,EAAQ,EAAS,MAAO,CAAQ,EAE7D,GAAI,CAAC,GAAY,EAAM,EAAU,CAAE,aAAY,CAAC,EAC9C,SAGF,IAAM,EAAQ,GAAa,CAAQ,GAE/B,CAAC,GAAQ,EAAQ,EAAK,SACxB,EAAO,CACL,MAAO,EAAS,MAChB,WACA,OACA,OAAQ,EAAS,QAAU,CAAC,EAC5B,QAAS,EAAS,QAClB,OAAQ,YACR,OACF,EAEJ,CAGF,OAAO,CACT,CAQA,SAAgB,GAAe,EAAyC,CACtE,IAAM,EAAS,EAAiB,KAC1B,EAAS,EAAO,UACnB,GAAS,EAAM,EAAS,WAAa,CAAI,GAAK,CACjD,EACM,EAAO,EAAM,EAAS,WAAa,EAAO,EAAO,EACjD,EACJ,EAAS,aAAe,EAAS,YACjC,EAAS,UAAU,SAAS,CAAC,EACzB,EAAQ,EAAO,KAAK,EAAG,IACvB,IAAU,EACL,EAGL,IAAU,EAAS,GAAM,GAAa,IAAU,EAAS,EACpD,EAAO,EAGT,EACR,EACK,EAAW,EAAW,CAAK,EAEjC,MAAO,CACL,MAAO,EAAS,MAChB,SAAU,EAAS,SACnB,KAAM,EAAgB,EAAQ,EAAO,CAAC,EACtC,OAAQ,EAAS,QAAU,CAAC,EAC5B,OAAQ,UACV,CACF,CAEA,IAAM,GAAe,IAAI,QAMzB,SAAgB,GACd,EACA,EACA,EACuB,CACvB,IAAI,EAAQ,GAAa,IAAI,CAAS,EAEjC,IACH,EAAQ,IAAI,IACZ,GAAa,IAAI,EAAW,CAAK,GAGnC,IAAM,EAAW,GAAG,EAAW,GAAG,EAAS,OAM3C,OAJK,EAAM,IAAI,CAAQ,GACrB,EAAM,IAAI,EAAU,GAAmB,EAAY,EAAU,CAAS,CAAC,EAGlE,EAAM,IAAI,CAAQ,CAC3B,CAEA,SAAS,GACP,EACA,EACA,EACuB,CACvB,GAAI,IAAe,OACjB,OAAO,GAAe,CAAQ,EAGhC,IAAM,EAAS,EAAiB,GAC1B,EAAc,IAAe,UAC7B,EACJ,IAAe,UAAY,IAAe,UACtC,GAAoB,EAAU,GAAa,EAAQ,EAAU,CAC3D,aACF,CAAC,EACD,KAIA,GACH,IAAe,mBACZ,KACA,GAAuB,EAAQ,EAAU,CACvC,cACA,SAAU,CACZ,CAAC,IAAM,GAAuB,EAAQ,EAAU,CAAE,aAAY,CAAC,EAErE,GACE,IACC,CAAC,GACA,EAAa,MAAQ,IACnB,GAAa,EAAW,GAAgB,CAAQ,CAAC,CAAC,GACtD,CACA,GAAM,CAAE,MAAO,EAAQ,GAAG,GAAY,EAEtC,OAAO,CACT,CAEA,OAAO,CACT,CAGA,SAAgB,GAAgB,EAAuC,CACrE,IAAM,EAAO,EAAyB,EAAS,WACzC,EAAO,EAAS,UAAU,IAAK,GAAS,EAAO,CAAI,EAErD,EAAS,aAAe,EAAS,YACnC,EAAK,QAAQ,GAA8B,EAAS,UAAU,EAGhE,IAAM,EAAS,CAAC,GAAG,IAAI,IAAI,CAAI,CAAC,CAAC,CAAC,MAAM,EAAM,IAAU,EAAO,CAAK,EAC9D,EAAS,EAAO,GAAK,EAAM,EAAO,EAAE,EACpC,EAAM,EAAO,EAAO,OAAS,GAEnC,MAAO,CACL,KAAM,EACN,SACA,QAAS,EAAM,EAAM,CAAG,EAAI,EAC9B,CACF,CAGA,SAAgB,GACd,EACA,EACA,EACoD,CACpD,IAAM,EAAO,EAAmB,CAAO,EAEvC,MAAO,CACL,MAAO,CACL,QAAS,EAAK,QACd,OAAQ,EAAK,MACf,EACA,SAAU,CACR,MAAO,EACP,QAAS,EAAQ,MAAM,OACvB,MAAO,EACP,SAAU,EAAQ,SAClB,qBAAsB,GACtB,OAAQ,CAAC,EACT,YAAa,IACb,WAAY,GACZ,kBAAmB,GACnB,cAAe,GACf,YAAa,IAAe,SAAW,GAAM,EAC/C,CACF,CACF,CAGA,SAAgB,GACd,EACA,EACA,EACW,CACX,IAAM,EAAO,OAAO,GAAS,EAAE,CAAC,CAAC,KAAK,EAChC,EAAW,GAAW,CAAI,EAEhC,GAAI,CAAC,EACH,MAAO,CAAE,KAAM,OAAQ,MAAO,EAAM,OAAQ,aAAc,EAG5D,IAAM,EAAS,EAAqB,CAAU,EAE9C,GAAI,IAAW,QACb,MAAO,CAAE,KAAM,QAAS,MAAO,EAAM,MAAO,GAAgB,CAAQ,CAAE,EAGxE,IAAM,EAAU,GAAkB,EAAQ,EAAU,CAAS,EAE7D,OAAO,EACH,CACE,KAAM,UACN,MAAO,EACP,UACA,MAAO,GAAc,EAAQ,EAAM,CAAO,CAC5C,EACA,CAAE,KAAM,OAAQ,MAAO,EAAM,OAAQ,YAAa,CACxD,CAGA,SAAgB,EAAiB,EAA2B,CAC1D,OAAQ,EAAK,KAAb,CACE,IAAK,UACH,OAAO,EAAK,QAAQ,KACtB,IAAK,QACH,OAAO,EAAK,MAAM,KACpB,QACE,MAAO,CAAC,CACZ,CACF,CAEA,IAAI,EAA+C,KAGnD,SAAgB,IAA8C,CAa5D,MAZA,KAAiB,QAAQ,IAAI,OAC3B,OAAO,qEACP,OAAO,+DACT,CAAC,CAAC,CAAC,MAAM,CAAC,EAAQ,MAAc,CAC9B,OAAU,EAAe,SAAW,EACpC,QAAW,EAAgB,SAAW,CACxC,EAAE,EAEF,EAAa,UAAY,CACvB,EAAe,IACjB,CAAC,EAEM,CACT,CCrpBA,OAAe,+ECAf,GAAe,+ECAf,GAAe,+ECAf,GAAe,+ECAf,GAAe,+ECAf,GAAe,+ECAf,GAAe,+ECAf,GAAe,+EC6Bf,SAAS,GAAyB,EAAO,CACvC,OAAO,OAAO,GAAS,EAAE,CAAC,CACvB,KAAK,CAAC,CACN,QAAQ,KAAM,GAAG,CAAC,CAClB,QAAQ,KAAM,GAAG,CACtB,CAMA,SAAS,GAAoB,EAAO,CAClC,OAAO,EAAc,GAAyB,CAAK,CAAC,GAAK,EAC3D,CAEA,SAAS,GAAoB,EAAO,CAClC,IAAM,EAAS,EAAc,CAAK,EAClC,OAAO,OAAO,SAAS,CAAM,EAAI,EAAS,IAC5C,CAEA,SAAS,GAAa,EAAS,CAC7B,IAAM,EAAoB,GAAyB,CAAO,EAE1D,GAAI,CAAC,EACH,OAAO,KAGT,IAAM,EAAQ,EAAkB,MAAM,0BAA0B,EAChE,GAAI,CAAC,EACH,OAAO,KAGT,IAAM,EAAQ,GAAoB,EAAM,EAAE,EAC1C,GAAI,CAAC,EACH,OAAO,KAGT,IAAM,EAAU,EAAM,KAAO,IACvB,EAAU,EAAU,EAAY,CAAK,EAAI,EAAY,CAAK,EAC1D,EAAS,GAAoB,EAAQ,KAAK,EAMhD,MAJI,CAAC,EAAQ,OAAS,GAAU,KACvB,KAGF,CACL,IAAK,GAAG,EAAQ,QAAQ,EAAU,IAAM,KACxC,MAAO,EAAQ,MACf,SACA,UACA,WAAY,OAAO,SAAS,EAAQ,UAAU,EAAI,EAAQ,WAAa,EACvE,aAAc,OAAO,EAAQ,cAAgB,EAAE,CACjD,CACF,CCCA,SAAS,EACP,EACA,EACA,EACA,EACgB,CAChB,MAAO,CACL,MACA,SAAU,SAAS,IACnB,MACA,YACA,OACF,CACF,CAEA,IAAa,EAAmB,CAC9B,YAAa,EACX,2BACA,yCACA,GACA,yEACF,EACA,YAAa,EACX,2BACA,yCACA,GACA,yEACF,EACA,WAAY,EACV,2BACA,sCACA,GACA,yEACF,EACA,eAAgB,EACd,2BACA,yCACA,GACA,yEACF,EACA,cAAe,EACb,2BACA,gCACA,GACA,yEACF,EACA,MAAO,EACL,2BACA,kCACA,GACA,yEACF,EACA,aAAc,EACZ,2BACA,+BACA,GACA,yEACF,EACA,aAAc,EACZ,2BACA,sCACA,GACA,yEACF,CACF,EAEa,EAAgD,CAC3D,CACE,GAAI,SACJ,MAAO,kBACP,UAAW,EAAiB,WAC9B,EACA,CACE,GAAI,mBACJ,MAAO,mBACP,UAAW,EAAiB,WAC9B,EACA,CACE,GAAI,kBACJ,MAAO,kBACP,UAAW,EAAiB,cAC9B,EACA,CAAE,GAAI,QAAS,MAAO,QAAS,UAAW,EAAiB,UAAW,EACtE,CACE,GAAI,iBACJ,MAAO,iBACP,UAAW,EAAiB,aAC9B,EACA,CAAE,GAAI,QAAS,MAAO,QAAS,UAAW,EAAiB,KAAM,EACjE,CACE,GAAI,UACJ,MAAO,iBACP,UAAW,EAAiB,WAC9B,EACA,CACE,GAAI,mBACJ,MAAO,0BACP,UAAW,EAAiB,WAC9B,EACA,CACE,GAAI,OACJ,MAAO,cACP,UAAW,EAAiB,YAC9B,EACA,CACE,GAAI,eACJ,MAAO,sBACP,UAAW,EAAiB,YAC9B,CACF,EAEM,GAAc,CAClB,IACA,KACA,IACA,KACA,IACA,IACA,KACA,IACA,KACA,IACA,KACA,GACF,EACM,GAAa,CACjB,IACA,KACA,IACA,KACA,IACA,IACA,KACA,IACA,KACA,IACA,KACA,GACF,EAEA,SAAgB,EAAmB,EAAoC,CACrE,OAAO,EAAkB,KAAM,GAAe,EAAW,KAAO,CAAE,GAAK,IACzE,CAOA,SAAgB,GACd,EACA,EACA,EACqB,CACrB,IAAM,EAAM,GAAa,CAAO,EAEhC,GAA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","names":["#context","#master","#room","#getBus","#stopVoice","#forget","#slots","#dampVoice","#groups","#buses","#addDrive","#runtime","#state","#statePatternId","#ensureContext","#ensurePlayer","#ensurePreset","#token","#setState","#start","#chordToken","#ensureEngine","#random","#sound","#playback","#clearUiTimers","#stopTimer","#records","#engine","#playerLoad","#presetLoads","#timer","#tick","#playStep","#showStep","#perform","#uiTimers","#strum","#emit","#databases","#listeners","#databaseLoad","#getInstrument","#defaultInstrument","#storageKey","#instrument","#readStoredInstrument","#player","#renderInstrumentField","#writeStoredInstrument","#applyInstrument","#sessions","#stopSession","#session","#detachSession","#playerUnsubscribes","#bindRoot","#decorate","#ownsPattern","#paint","#playCard","#handleState","#handleStep","#instrumentSelects","#renderChordView","#addPlayButton","#followRender","#createPlayButton","#togglePattern","#playChord","#fillPopupActions","#instrumentLabel","#createViewToggle","#paintButton","#chordViewOpen","#chordView","#drawChordView","#createViewMessage","#stepInstrument","#createViewShell","#container","#ownsPlayer","#theme","#checkTheme","#prefix","#settings","#root","#applyTheme","#teardown","#abort","#refs","#wire","#renderSongControls","#renderInstruments","#renderVoicing","#renderPatterns","#loadChords","#patterns","#unsubscribe","#chordLibrary","#currentStep","#setStatus","#getVoicing","#syncPlaybackState","#findCard","#startPattern","#saveSettings","#idCounter","#createPatternId","#createLabeler","#showCurrentStep","#onPlayerState","#removePattern","#cycleStep","#restartIfPlaying","#setTempo","#addPatterns","#restoreExamplePatterns"],"ignoreList":[],"sources":["../../../src/plugins/OpenLyric/domain/chord-voicings.ts","../../../src/plugins/OpenLyricPlayer/chord-library.ts","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0000_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0040_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0170_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0240_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0250_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0270_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0320_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0340_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyric/domain/music-theory.ts","../../../src/plugins/OpenLyricPlayer/instruments.ts","../../../src/plugins/OpenLyricPlayer/patterns.ts","../../../src/plugins/OpenLyricPlayer/instrument-sounds.ts","../../../src/plugins/OpenLyricPlayer/strum-plan.ts","../../../src/plugins/OpenLyricPlayer/StrumSoundEngine.ts","../../../src/plugins/OpenLyricPlayer/webaudiofont.ts","../../../src/plugins/OpenLyricPlayer/assets/webaudiofont/WebAudioFontPlayer.js?url","../../../src/plugins/OpenLyricPlayer/StrumPlayer.ts","../../../src/plugins/OpenLyricPlayer/OpenLyricPlayer.ts","../../../src/plugins/OpenLyricPlayer/chord-figure.ts","../../../src/plugins/OpenLyricPlayer/OpenLyricPluginPlayer.ts","../../../src/plugins/OpenLyricPlayer/playground-chords.ts","../../../src/plugins/OpenLyricPlayer/playground-markup.ts","../../../src/plugins/OpenLyricPlayer/OpenLyricPlayerPlayground.ts","../src/version.ts"],"sourcesContent":["/**\r\n * Chord positions from `@tombatossals/chords-db` (MIT): ranking a chord's\r\n * positions from easiest to hardest and turning one into SVGuitar data. Shared\r\n * by the editor's chord popup (guitar) and the strum player (guitar, ukulele).\r\n *\r\n * A position lists one fret per string, lowest string first (`-1` muted,\r\n * `0` open), relative to `baseFret`; `barres` names the relative frets a\r\n * finger lays across.\r\n */\r\n\r\nexport interface ChordDatabasePosition {\r\n frets: number[];\r\n fingers?: number[];\r\n baseFret?: number;\r\n barres?: number[];\r\n capo?: boolean;\r\n midi?: number[];\r\n}\r\n\r\nexport interface ChordDatabaseEntry {\r\n key: string;\r\n suffix: string;\r\n positions: ChordDatabasePosition[];\r\n}\r\n\r\nexport interface ChordDatabase {\r\n chords: Record<string, ChordDatabaseEntry[]>;\r\n}\r\n\r\nexport interface ChordDatabaseSearch {\r\n datasetRoot: string;\r\n suffixCandidates: string[];\r\n}\r\n\r\nfunction getStringCount(position: ChordDatabasePosition) {\r\n return Array.isArray(position?.frets) ? position.frets.length : 6;\r\n}\r\n\r\nfunction getLowestPitchClass(position: ChordDatabasePosition) {\r\n if (!Array.isArray(position?.midi) || !position.midi.length) {\r\n return null;\r\n }\r\n\r\n const lowestMidi = position.midi[0];\r\n return ((lowestMidi % 12) + 12) % 12;\r\n}\r\n\r\n/** Lower is easier: low on the neck, few barres, a small span, open strings. */\r\nfunction scorePosition(position: ChordDatabasePosition) {\r\n const frets = Array.isArray(position?.frets) ? position.frets : [];\r\n const openStringCount = frets.filter((fret) => fret === 0).length;\r\n const mutedStringCount = frets.filter((fret) => fret < 0).length;\r\n const frettedNotes = frets.filter((fret) => fret > 0);\r\n const fretSpan = frettedNotes.length\r\n ? Math.max(...frettedNotes) - Math.min(...frettedNotes)\r\n : 0;\r\n\r\n return (\r\n Math.max((position.baseFret || 1) - 1, 0) * 12 +\r\n (Array.isArray(position?.barres) ? position.barres.length : 0) * 3 +\r\n fretSpan * 2 +\r\n frettedNotes.length +\r\n mutedStringCount * 0.5 -\r\n openStringCount * 1.5\r\n );\r\n}\r\n\r\n/**\r\n * Easiest first; with a slash chord's bass pitch class, positions whose lowest\r\n * note is that bass come first.\r\n */\r\nfunction rankChordPositions<T extends ChordDatabasePosition>(\r\n positions: T[],\r\n bassPitchClass?: number | null,\r\n): T[] {\r\n if (!Array.isArray(positions) || !positions.length) {\r\n return [];\r\n }\r\n\r\n return positions\r\n .map((position, index) => ({\r\n bassMatches:\r\n typeof bassPitchClass === 'number'\r\n ? getLowestPitchClass(position) === bassPitchClass\r\n : false,\r\n index,\r\n position,\r\n score: scorePosition(position),\r\n }))\r\n .sort((left, right) => {\r\n if (left.bassMatches !== right.bassMatches) {\r\n return Number(right.bassMatches) - Number(left.bassMatches);\r\n }\r\n\r\n if (left.score !== right.score) {\r\n return left.score - right.score;\r\n }\r\n\r\n return left.index - right.index;\r\n })\r\n .map((entry) => entry.position);\r\n}\r\n\r\nfunction deriveBarres(position: ChordDatabasePosition) {\r\n const frets = Array.isArray(position?.frets) ? position.frets : [];\r\n const fingers = Array.isArray(position?.fingers) ? position.fingers : [];\r\n const barres = Array.isArray(position?.barres) ? position.barres : [];\r\n const stringCount = getStringCount(position);\r\n\r\n return barres\r\n .map((barreFret) => {\r\n const matchingEntries = frets\r\n .map((fret, index) => ({\r\n fret,\r\n index,\r\n finger: fingers[index] || 0,\r\n }))\r\n .filter((entry) => entry.fret === barreFret);\r\n\r\n if (!matchingEntries.length) {\r\n return null;\r\n }\r\n\r\n const targetFinger =\r\n matchingEntries.find((entry) => entry.finger > 0)?.finger || 0;\r\n const filteredEntries = targetFinger\r\n ? matchingEntries.filter((entry) => entry.finger === targetFinger)\r\n : matchingEntries;\r\n const fromIndex = filteredEntries[0]?.index;\r\n const toIndex = filteredEntries[filteredEntries.length - 1]?.index;\r\n\r\n if (\r\n typeof fromIndex !== 'number' ||\r\n typeof toIndex !== 'number' ||\r\n fromIndex === toIndex\r\n ) {\r\n return null;\r\n }\r\n\r\n const barreFinger = targetFinger > 0 ? String(targetFinger) : undefined;\r\n\r\n return {\r\n fromString: stringCount - fromIndex,\r\n toString: stringCount - toIndex,\r\n fret: barreFret,\r\n style: 'arc' as const,\r\n text: barreFinger,\r\n };\r\n })\r\n .filter((barre) => barre !== null);\r\n}\r\n\r\n/** SVGuitar's `{ fingers, barres, position }` for a position. */\r\nfunction createSvguitarData(position: ChordDatabasePosition) {\r\n const fingers: [number, number | 'x', string?][] = [];\r\n const stringCount = getStringCount(position);\r\n\r\n for (const [index, fret] of (position?.frets || []).entries()) {\r\n const stringNumber = stringCount - index;\r\n const fingerNumber = position?.fingers?.[index] || 0;\r\n\r\n if (fret < 0) {\r\n fingers.push([stringNumber, 'x']);\r\n continue;\r\n }\r\n\r\n if (fret === 0) {\r\n fingers.push([stringNumber, 0]);\r\n continue;\r\n }\r\n\r\n fingers.push([\r\n stringNumber,\r\n fret,\r\n fingerNumber > 0 ? String(fingerNumber) : undefined,\r\n ]);\r\n }\r\n\r\n return {\r\n fingers,\r\n position: Math.max(position?.baseFret || 1, 1),\r\n barres: deriveBarres(position),\r\n };\r\n}\r\n\r\n/**\r\n * The database entry for the first suffix candidate it has. By default a chord\r\n * it has no suffix for falls back to the plain major entry (what the editor's\r\n * popup shows); `allowMajorFallback: false` returns `null` instead.\r\n */\r\nfunction resolveMatchingChordEntry(\r\n database: ChordDatabase | null | undefined,\r\n search: ChordDatabaseSearch,\r\n { allowMajorFallback = true }: { allowMajorFallback?: boolean } = {},\r\n): ChordDatabaseEntry | null {\r\n const chordEntries = database?.chords?.[search.datasetRoot];\r\n if (!Array.isArray(chordEntries) || !chordEntries.length) {\r\n return null;\r\n }\r\n\r\n for (const suffixCandidate of search.suffixCandidates) {\r\n const matchingEntry = chordEntries.find(\r\n (entry) => entry.suffix === suffixCandidate,\r\n );\r\n\r\n if (matchingEntry) {\r\n return matchingEntry;\r\n }\r\n }\r\n\r\n return allowMajorFallback\r\n ? chordEntries.find((entry) => entry.suffix === 'major') || null\r\n : null;\r\n}\r\n\r\nexport {\r\n createSvguitarData,\r\n deriveBarres,\r\n getLowestPitchClass,\r\n rankChordPositions,\r\n resolveMatchingChordEntry,\r\n scorePosition,\r\n};\r\n","import { get as getTonalChord } from '@tonaljs/chord';\r\nimport { get as getInterval } from '@tonaljs/interval';\r\nimport { chroma as getNoteChroma } from '@tonaljs/note';\r\nimport type { Barre, Chord, ChordSettings, Finger } from 'svguitar';\r\n\r\nimport { CHORD_KEY_GROUPS } from '../../editor/scripts/shared.ts';\r\nimport {\r\n createSvguitarData,\r\n scorePosition,\r\n type ChordDatabase,\r\n type ChordDatabasePosition,\r\n} from '../OpenLyric/domain/chord-voicings.ts';\r\nimport type { StrumInstrumentId } from './instruments.ts';\r\n\r\n/**\r\n * A voicing for every chord Open Lyric offers, on each of the strum player's\r\n * instruments — what a chord card draws and plays, and what a pattern strums\r\n * for the song key. Five instruments voice chords; the others borrow one's\r\n * voicings ({@link getVoicingInstrument}).\r\n *\r\n * - **Guitar and ukulele:** the easiest of the positions\r\n * `@tombatossals/chords-db` (MIT, the database the editor's chord popup\r\n * draws from) has for the chord and the one {@link generateFrettedVoicing}\r\n * computes — only those that really spell the chord ({@link spellsChord}),\r\n * ranked by the popup's `scorePosition`.\r\n * - **Baritone ukulele:** computed by {@link generateFrettedVoicing} on its\r\n * standard tuning.\r\n * - **Bass:** the chord's bass note low on the neck, with its fifth and\r\n * octave above ({@link getBassVoicing}).\r\n * - **Piano:** the chord's notes stacked in close root position from octave 3,\r\n * a slash chord's bass an octave below.\r\n *\r\n * The lowest note of a guitar or baritone voicing is always the chord's bass\r\n * (its root, or a slash chord's bass). A ukulele is re-entrant — its G string\r\n * is tuned above its C — so it has no bass, and any inversion will do.\r\n *\r\n * Chords are read by Tonal, the music theory the editor uses. A name it cannot\r\n * read (`Cmaj2`, `Am4`, …) has no voicing on any instrument.\r\n */\r\n\r\n/** Open-string MIDI notes, string order (lowest-pitched course first, except\r\n * the re-entrant ukulele), in standard tuning. */\r\nexport const STANDARD_TUNINGS = {\r\n guitar: [40, 45, 50, 55, 59, 64], // E2 A2 D3 G3 B3 E4\r\n ukulele: [67, 60, 64, 69], // G4 C4 E4 A4 — re-entrant\r\n 'baritone-ukulele': [50, 55, 59, 64], // D3 G3 B3 E4\r\n bass: [28, 33, 38, 43], // E1 A1 D2 G2\r\n} as const satisfies Record<string, readonly number[]>;\r\n\r\nexport type FrettedInstrumentId = keyof typeof STANDARD_TUNINGS;\r\n\r\n/** The instruments whose voicings the others borrow. */\r\nexport type VoicingInstrumentId = FrettedInstrumentId | 'piano';\r\n\r\n/**\r\n * The instrument whose voicings `instrument` plays: a classical or electric\r\n * guitar voices a chord as a guitar does, an electric piano or organ as a\r\n * piano, an upright bass as a bass.\r\n */\r\nexport function getVoicingInstrument(\r\n instrument: StrumInstrumentId,\r\n): VoicingInstrumentId {\r\n switch (instrument) {\r\n case 'classical-guitar':\r\n case 'electric-guitar':\r\n return 'guitar';\r\n case 'electric-piano':\r\n case 'organ':\r\n return 'piano';\r\n case 'upright-bass':\r\n return 'bass';\r\n default:\r\n return instrument;\r\n }\r\n}\r\n\r\nexport interface ChordDatabases {\r\n guitar: ChordDatabase;\r\n ukulele: ChordDatabase;\r\n}\r\n\r\nexport interface OpenLyricChordGroup {\r\n name: string;\r\n chords: string[];\r\n}\r\n\r\n/** What Open Lyric makes of a chord name. */\r\nexport interface ChordSpelling {\r\n name: string;\r\n rootChroma: number;\r\n /** The lowest note: a slash chord's bass, else the root. */\r\n bassChroma: number;\r\n /** Every pitch class of the chord, bass included. */\r\n chromas: number[];\r\n /** Pitch classes a voicing must sound — all but the perfect fifth and inner\r\n * extensions a player may leave out. */\r\n requiredChromas: number[];\r\n /** The chord's intervals above the root, in semitones, low to high. */\r\n semitones: number[];\r\n}\r\n\r\nexport interface FrettedVoicing {\r\n /** One fret per string, string order; `-1` muted, relative to `baseFret`. */\r\n frets: number[];\r\n baseFret: number;\r\n /** MIDI notes in string order, muted strings left out. */\r\n midi: number[];\r\n /** Relative frets a finger lays across. */\r\n barres: number[];\r\n /** chords-db fingerings, one per string (0 = none); computed voicings have none. */\r\n fingers?: number[];\r\n source: 'chords-db' | 'computed';\r\n}\r\n\r\nexport interface PianoVoicing {\r\n /** The keys played, low to high. */\r\n midi: number[];\r\n /** The first and last key of the board drawn. */\r\n lowest: number;\r\n highest: number;\r\n}\r\n\r\nexport type ChordCard =\r\n | {\r\n kind: 'diagram';\r\n chord: string;\r\n voicing: FrettedVoicing;\r\n chart: { chord: Chord; settings: Partial<ChordSettings> };\r\n }\r\n | { kind: 'piano'; chord: string; piano: PianoVoicing }\r\n | {\r\n kind: 'none';\r\n chord: string;\r\n /** `not-a-chord`: Tonal cannot read the name. `no-voicing`: nothing\r\n * playable was found for this instrument. */\r\n reason: 'not-a-chord' | 'no-voicing';\r\n };\r\n\r\nconst PIANO_ROOT_OCTAVE_MIDI = 48; // C3\r\nconst MAX_FRET = 12;\r\nconst FRET_SPAN = 3;\r\nconst MAX_FINGERS = 4;\r\nconst MUTED_STRING_COST = 1.5;\r\n/**\r\n * How much easier a computed voicing must score to beat a chords-db position,\r\n * whose shapes are curated and come with fingerings. Half a fret up the neck\r\n * scores 6.\r\n */\r\nconst DATABASE_PREFERENCE = 6;\r\n\r\n/**\r\n * Open Lyric's chord list (`CHORD_KEY_GROUPS`, the chords its editor\r\n * suggests), with each `F# / Gb` pair split into its two spellings.\r\n */\r\nexport function getOpenLyricChordGroups(): OpenLyricChordGroup[] {\r\n const keyGroups: readonly { name: string; keys: readonly string[] }[] =\r\n CHORD_KEY_GROUPS;\r\n\r\n return keyGroups.map((group) => ({\r\n name: group.name,\r\n chords: group.keys.flatMap((key) =>\r\n key.split('/').map((spelling) => spelling.trim()),\r\n ),\r\n }));\r\n}\r\n\r\nfunction mod12(value: number) {\r\n return ((value % 12) + 12) % 12;\r\n}\r\n\r\n/**\r\n * Whether an interval is one a voicing may drop: the perfect fifth, and any\r\n * 9th, 11th or 13th below the chord's top extension.\r\n */\r\nfunction isOptionalInterval(name: string, isTop: boolean) {\r\n const interval = getInterval(name);\r\n\r\n if (interval.empty) {\r\n return false;\r\n }\r\n\r\n return (\r\n (interval.simple === 5 && interval.q === 'P') ||\r\n (!isTop && [9, 11, 13].includes(interval.num))\r\n );\r\n}\r\n\r\n/** `C#m7/G#` → its root, bass and pitch classes, or `null` if unreadable. */\r\nexport function spellChord(chord: string): ChordSpelling | null {\r\n const name = String(chord ?? '').trim();\r\n const [head, bass] = name.split('/');\r\n const tonal = getTonalChord(head);\r\n const rootChroma = getNoteChroma(tonal.tonic ?? '');\r\n\r\n if (tonal.empty || !tonal.tonic || !Number.isFinite(rootChroma)) {\r\n return null;\r\n }\r\n\r\n const bassChroma = bass ? getNoteChroma(bass) : rootChroma;\r\n\r\n if (!Number.isFinite(bassChroma)) {\r\n return null;\r\n }\r\n\r\n const semitones = tonal.intervals.map(\r\n (interval) => getInterval(interval).semitones ?? 0,\r\n );\r\n const intervalChromas = semitones.map((step) => mod12(rootChroma + step));\r\n const required = tonal.intervals\r\n .filter(\r\n (interval, index) =>\r\n !isOptionalInterval(interval, index === tonal.intervals.length - 1),\r\n )\r\n .map((interval) =>\r\n mod12(rootChroma + (getInterval(interval).semitones ?? 0)),\r\n );\r\n\r\n return {\r\n name,\r\n rootChroma,\r\n bassChroma,\r\n chromas: [...new Set([bassChroma, ...intervalChromas])],\r\n requiredChromas: [...new Set([bassChroma, ...required])],\r\n semitones,\r\n };\r\n}\r\n\r\n/**\r\n * A voicing spells a chord when it sounds nothing outside it, sounds every\r\n * required note and — unless `requireBass` is off — has the chord's bass as\r\n * its lowest note.\r\n */\r\nexport function spellsChord(\r\n midi: readonly number[],\r\n spelling: ChordSpelling,\r\n { requireBass = true }: { requireBass?: boolean } = {},\r\n): boolean {\r\n if (!midi.length) {\r\n return false;\r\n }\r\n\r\n const sounding = new Set(midi.map(mod12));\r\n\r\n return (\r\n [...sounding].every((chroma) => spelling.chromas.includes(chroma)) &&\r\n spelling.requiredChromas.every((chroma) => sounding.has(chroma)) &&\r\n (!requireBass || mod12(Math.min(...midi)) === spelling.bassChroma)\r\n );\r\n}\r\n\r\n/**\r\n * How hard a voicing is to play, lower being easier: the chord popup's\r\n * `scorePosition`, plus a cost for every muted string — a strummed chord\r\n * should ring on as many strings as it can.\r\n */\r\nfunction scoreVoicing(position: ChordDatabasePosition) {\r\n const muted = position.frets.filter((fret) => fret < 0).length;\r\n\r\n return scorePosition(position) + muted * MUTED_STRING_COST;\r\n}\r\n\r\n/** MIDI notes of a position, string order, muted strings left out. */\r\nexport function getPositionMidi(\r\n tuning: readonly number[],\r\n frets: readonly number[],\r\n baseFret: number,\r\n): number[] {\r\n const offset = Math.max(baseFret, 1) - 1;\r\n\r\n return frets.flatMap((fret, index) =>\r\n fret < 0 ? [] : [tuning[index] + (fret === 0 ? 0 : fret + offset)],\r\n );\r\n}\r\n\r\nfunction* combine(\r\n options: readonly (readonly number[])[],\r\n index = 0,\r\n chosen: number[] = [],\r\n): Generator<number[]> {\r\n if (index === options.length) {\r\n yield [...chosen];\r\n return;\r\n }\r\n\r\n for (const option of options[index]) {\r\n chosen.push(option);\r\n yield* combine(options, index + 1, chosen);\r\n chosen.pop();\r\n }\r\n}\r\n\r\n/**\r\n * The easiest playable voicing of `spelling` on a fretted instrument, found by\r\n * trying every fingering in each four-fret window of the neck:\r\n *\r\n * - at least `minNotes` strings sound, each a chord note, next to each\r\n * other so the chord can be strummed;\r\n * - the voicing spells the chord ({@link spellsChord}, with `requireBass`);\r\n * - no more than four fingers, a barre across the lowest fret counting as one.\r\n *\r\n * Ranked like chords-db positions ({@link scoreVoicing}); `null` when nothing\r\n * fits.\r\n */\r\nexport function generateFrettedVoicing(\r\n tuning: readonly number[],\r\n spelling: ChordSpelling,\r\n {\r\n requireBass = true,\r\n minNotes = Math.min(3, tuning.length),\r\n }: { requireBass?: boolean; minNotes?: number } = {},\r\n): FrettedVoicing | null {\r\n let best: { frets: number[]; score: number } | null = null;\r\n\r\n for (let low = 1; low + FRET_SPAN <= MAX_FRET; low += 1) {\r\n const window = [\r\n 0,\r\n ...Array.from({ length: FRET_SPAN + 1 }, (_, offset) => low + offset),\r\n ];\r\n const options = tuning.map((open) => [\r\n -1,\r\n ...window.filter((fret) => spelling.chromas.includes(mod12(open + fret))),\r\n ]);\r\n\r\n for (const frets of combine(options)) {\r\n const first = frets.findIndex((fret) => fret >= 0);\r\n const last = frets.findLastIndex((fret) => fret >= 0);\r\n const sounding = last - first + 1;\r\n\r\n if (\r\n first === -1 ||\r\n sounding < minNotes ||\r\n frets.slice(first, last + 1).includes(-1)\r\n ) {\r\n continue;\r\n }\r\n\r\n const fretted = frets.filter((fret) => fret > 0);\r\n const lowestFret = Math.min(...fretted);\r\n const fingers =\r\n fretted.length > MAX_FINGERS\r\n ? fretted.filter((fret) => fret !== lowestFret).length + 1\r\n : fretted.length;\r\n\r\n if (\r\n fingers > MAX_FINGERS ||\r\n !spellsChord(getPositionMidi(tuning, frets, 1), spelling, {\r\n requireBass,\r\n })\r\n ) {\r\n continue;\r\n }\r\n\r\n const score = scoreVoicing(toPosition(frets));\r\n\r\n if (!best || score < best.score) {\r\n best = { frets, score };\r\n }\r\n }\r\n }\r\n\r\n if (!best) {\r\n return null;\r\n }\r\n\r\n const position = toPosition(best.frets);\r\n\r\n return {\r\n frets: position.frets,\r\n baseFret: position.baseFret!,\r\n midi: getPositionMidi(tuning, best.frets, 1),\r\n barres: position.barres ?? [],\r\n source: 'computed',\r\n };\r\n}\r\n\r\n/** Absolute frets → a chords-db-style position, relative to its base fret. */\r\nfunction toPosition(frets: readonly number[]): ChordDatabasePosition {\r\n const fretted = frets.filter((fret) => fret > 0);\r\n const highest = fretted.length ? Math.max(...fretted) : 0;\r\n const lowest = fretted.length ? Math.min(...fretted) : 0;\r\n const baseFret = highest <= 4 ? 1 : lowest;\r\n const relative = frets.map((fret) => (fret > 0 ? fret - baseFret + 1 : fret));\r\n const relativeFretted = relative.filter((fret) => fret > 0);\r\n\r\n return {\r\n frets: relative,\r\n baseFret,\r\n barres:\r\n relativeFretted.length > MAX_FINGERS\r\n ? [Math.min(...relativeFretted)]\r\n : [],\r\n };\r\n}\r\n\r\n/** A voicing's frets counted from the nut. */\r\nfunction toAbsoluteFrets(voicing: FrettedVoicing): number[] {\r\n return voicing.frets.map((fret) =>\r\n fret > 0 ? fret + voicing.baseFret - 1 : fret,\r\n );\r\n}\r\n\r\n/** chords-db names its roots `C`, `Csharp`/`Db`, …; find the one for a chroma. */\r\nfunction getDatabaseRoot(database: ChordDatabase, chroma: number) {\r\n return Object.keys(database.chords).find(\r\n (root) => getNoteChroma(root.replace('sharp', '#')) === chroma,\r\n );\r\n}\r\n\r\n/** The easiest chords-db position that spells the chord, or `null`. */\r\nexport function findDatabaseVoicing(\r\n database: ChordDatabase,\r\n tuning: readonly number[],\r\n spelling: ChordSpelling,\r\n { requireBass = true }: { requireBass?: boolean } = {},\r\n): (FrettedVoicing & { score: number }) | null {\r\n const root = getDatabaseRoot(database, spelling.rootChroma);\r\n let best: (FrettedVoicing & { score: number }) | null = null;\r\n\r\n for (const entry of root ? database.chords[root] : []) {\r\n for (const position of entry.positions) {\r\n const baseFret = position.baseFret ?? 1;\r\n const midi = getPositionMidi(tuning, position.frets, baseFret);\r\n\r\n if (!spellsChord(midi, spelling, { requireBass })) {\r\n continue;\r\n }\r\n\r\n const score = scoreVoicing(position);\r\n\r\n if (!best || score < best.score) {\r\n best = {\r\n frets: position.frets,\r\n baseFret,\r\n midi,\r\n barres: position.barres ?? [],\r\n fingers: position.fingers,\r\n source: 'chords-db',\r\n score,\r\n };\r\n }\r\n }\r\n }\r\n\r\n return best;\r\n}\r\n\r\n/**\r\n * A bass line for the chord: its bass note on the lowest string that reaches\r\n * it within the first four frets, then — for a chord with a perfect fifth and\r\n * no slash bass — the fifth two frets up on the next string, and the octave\r\n * two frets up on the string after that.\r\n */\r\nexport function getBassVoicing(spelling: ChordSpelling): FrettedVoicing {\r\n const tuning = STANDARD_TUNINGS.bass;\r\n const string = tuning.findIndex(\r\n (open) => mod12(spelling.bassChroma - open) <= 4,\r\n );\r\n const fret = mod12(spelling.bassChroma - tuning[string]);\r\n const withFifth =\r\n spelling.bassChroma === spelling.rootChroma &&\r\n spelling.semitones.includes(7);\r\n const frets = tuning.map((_, index) => {\r\n if (index === string) {\r\n return fret;\r\n }\r\n\r\n if (index === string + 2 || (withFifth && index === string + 1)) {\r\n return fret + 2;\r\n }\r\n\r\n return -1;\r\n });\r\n const position = toPosition(frets);\r\n\r\n return {\r\n frets: position.frets,\r\n baseFret: position.baseFret!,\r\n midi: getPositionMidi(tuning, frets, 1),\r\n barres: position.barres ?? [],\r\n source: 'computed',\r\n };\r\n}\r\n\r\nconst voicingCache = new WeakMap<\r\n ChordDatabases,\r\n Map<string, FrettedVoicing | null>\r\n>();\r\n\r\n/** The voicing a fretted instrument plays for `spelling`, or `null`. */\r\nexport function getFrettedVoicing(\r\n instrument: FrettedInstrumentId,\r\n spelling: ChordSpelling,\r\n databases: ChordDatabases,\r\n): FrettedVoicing | null {\r\n let cache = voicingCache.get(databases);\r\n\r\n if (!cache) {\r\n cache = new Map();\r\n voicingCache.set(databases, cache);\r\n }\r\n\r\n const cacheKey = `${instrument}|${spelling.name}`;\r\n\r\n if (!cache.has(cacheKey)) {\r\n cache.set(cacheKey, findFrettedVoicing(instrument, spelling, databases));\r\n }\r\n\r\n return cache.get(cacheKey)!;\r\n}\r\n\r\nfunction findFrettedVoicing(\r\n instrument: FrettedInstrumentId,\r\n spelling: ChordSpelling,\r\n databases: ChordDatabases,\r\n): FrettedVoicing | null {\r\n if (instrument === 'bass') {\r\n return getBassVoicing(spelling);\r\n }\r\n\r\n const tuning = STANDARD_TUNINGS[instrument];\r\n const requireBass = instrument !== 'ukulele';\r\n const fromDatabase =\r\n instrument === 'guitar' || instrument === 'ukulele'\r\n ? findDatabaseVoicing(databases[instrument], tuning, spelling, {\r\n requireBass,\r\n })\r\n : null;\r\n // A ukulele strums all four strings and a guitar at least four when a shape\r\n // allows; otherwise — and on a baritone ukulele, whose open-position shapes\r\n // often leave the low D out — three will do.\r\n const computed =\r\n (instrument === 'baritone-ukulele'\r\n ? null\r\n : generateFrettedVoicing(tuning, spelling, {\r\n requireBass,\r\n minNotes: 4,\r\n })) ?? generateFrettedVoicing(tuning, spelling, { requireBass });\r\n\r\n if (\r\n fromDatabase &&\r\n (!computed ||\r\n fromDatabase.score - DATABASE_PREFERENCE <=\r\n scoreVoicing(toPosition(toAbsoluteFrets(computed))))\r\n ) {\r\n const { score: _score, ...voicing } = fromDatabase;\r\n\r\n return voicing;\r\n }\r\n\r\n return computed;\r\n}\r\n\r\n/** The chord in close root position from octave 3, slash bass below. */\r\nexport function getPianoVoicing(spelling: ChordSpelling): PianoVoicing {\r\n const root = PIANO_ROOT_OCTAVE_MIDI + spelling.rootChroma;\r\n const midi = spelling.semitones.map((step) => root + step);\r\n\r\n if (spelling.bassChroma !== spelling.rootChroma) {\r\n midi.unshift(PIANO_ROOT_OCTAVE_MIDI - 12 + spelling.bassChroma);\r\n }\r\n\r\n const sorted = [...new Set(midi)].sort((left, right) => left - right);\r\n const lowest = sorted[0] - mod12(sorted[0]);\r\n const top = sorted[sorted.length - 1];\r\n\r\n return {\r\n midi: sorted,\r\n lowest,\r\n highest: top - mod12(top) + 11,\r\n };\r\n}\r\n\r\n/** SVGuitar data and settings for a fretted voicing. */\r\nexport function getChordChart(\r\n instrument: FrettedInstrumentId,\r\n chord: string,\r\n voicing: FrettedVoicing,\r\n): { chord: Chord; settings: Partial<ChordSettings> } {\r\n const data = createSvguitarData(voicing);\r\n\r\n return {\r\n chord: {\r\n fingers: data.fingers as Finger[],\r\n barres: data.barres as Barre[],\r\n },\r\n settings: {\r\n title: chord,\r\n strings: voicing.frets.length,\r\n frets: 4,\r\n position: voicing.baseFret,\r\n fixedDiagramPosition: true,\r\n tuning: [],\r\n strokeWidth: 1.5,\r\n fingerSize: 0.7,\r\n fretLabelFontSize: 40,\r\n titleFontSize: 48,\r\n sidePadding: instrument === 'guitar' ? 0.2 : 0.3,\r\n },\r\n };\r\n}\r\n\r\n/** What a chord card shows, and plays, for `chord` on `instrument`. */\r\nexport function resolveChordCard(\r\n instrument: StrumInstrumentId,\r\n chord: string,\r\n databases: ChordDatabases,\r\n): ChordCard {\r\n const name = String(chord ?? '').trim();\r\n const spelling = spellChord(name);\r\n\r\n if (!spelling) {\r\n return { kind: 'none', chord: name, reason: 'not-a-chord' };\r\n }\r\n\r\n const family = getVoicingInstrument(instrument);\r\n\r\n if (family === 'piano') {\r\n return { kind: 'piano', chord: name, piano: getPianoVoicing(spelling) };\r\n }\r\n\r\n const voicing = getFrettedVoicing(family, spelling, databases);\r\n\r\n return voicing\r\n ? {\r\n kind: 'diagram',\r\n chord: name,\r\n voicing,\r\n chart: getChordChart(family, name, voicing),\r\n }\r\n : { kind: 'none', chord: name, reason: 'no-voicing' };\r\n}\r\n\r\n/** The notes a card plays, in the order they are struck; empty for none. */\r\nexport function getChordCardMidi(card: ChordCard): number[] {\r\n switch (card.kind) {\r\n case 'diagram':\r\n return card.voicing.midi;\r\n case 'piano':\r\n return card.piano.midi;\r\n default:\r\n return [];\r\n }\r\n}\r\n\r\nlet databaseLoad: Promise<ChordDatabases> | null = null;\r\n\r\n/** Loads the chords-db guitar and ukulele tables once, on demand. */\r\nexport function loadChordDatabases(): Promise<ChordDatabases> {\r\n databaseLoad ??= Promise.all([\r\n import('@tombatossals/chords-db/lib/guitar.json'),\r\n import('@tombatossals/chords-db/lib/ukulele.json'),\r\n ]).then(([guitar, ukulele]) => ({\r\n guitar: ((guitar as any).default ?? guitar) as ChordDatabase,\r\n ukulele: ((ukulele as any).default ?? ukulele) as ChordDatabase,\r\n }));\r\n\r\n databaseLoad.catch(() => {\r\n databaseLoad = null;\r\n });\r\n\r\n return databaseLoad;\r\n}\r\n","export default \"__VITE_ASSET__$oUiuAXR7gZMKRQU56HW2w__\"","export default \"__VITE_ASSET__VRAku6fjghkApIISiBWPzg__\"","export default \"__VITE_ASSET__DUq8YoikZLXEejpNZ1KQvg__\"","export default \"__VITE_ASSET__8HSUfh7cJHNFxCkInNzNXw__\"","export default \"__VITE_ASSET__P4DPfwIRieI8P6q8r4JZRw__\"","export default \"__VITE_ASSET__WPGYjwIlzWVNM1bYhOc83w__\"","export default \"__VITE_ASSET__FKNdLzKnlftW3t_gzo81yw__\"","export default \"__VITE_ASSET__cn7IAAGY294pWGc7MpElVg__\"","import { get as getChord, tokenize as tokenizeChord } from '@tonaljs/chord';\r\nimport {\r\n fromSemitones as intervalFromSemitones,\r\n semitones as intervalSemitones,\r\n} from '@tonaljs/interval';\r\nimport { majorKey as getMajorKey, minorKey as getMinorKey } from '@tonaljs/key';\r\nimport {\r\n chroma as getNoteChroma,\r\n enharmonic as getEnharmonicNote,\r\n pitchClass as getPitchClass,\r\n simplify as simplifyNote,\r\n transpose as transposeNote,\r\n} from '@tonaljs/note';\r\n\r\nconst CHORD_DATABASE_ROOTS_BY_CHROMA = [\r\n 'C',\r\n 'Csharp',\r\n 'D',\r\n 'Eb',\r\n 'E',\r\n 'F',\r\n 'Fsharp',\r\n 'G',\r\n 'Ab',\r\n 'A',\r\n 'Bb',\r\n 'B',\r\n];\r\n\r\nfunction normalizeMusicTheoryText(value) {\r\n return String(value ?? '')\r\n .trim()\r\n .replace(/♯/g, '#')\r\n .replace(/♭/g, 'b');\r\n}\r\n\r\nfunction normalizeChordText(value) {\r\n return normalizeMusicTheoryText(value).replace(/\\s+/g, '');\r\n}\r\n\r\nfunction normalizePitchClass(value) {\r\n return getPitchClass(normalizeMusicTheoryText(value)) || '';\r\n}\r\n\r\nfunction getPitchClassChroma(value) {\r\n const chroma = getNoteChroma(value);\r\n return Number.isFinite(chroma) ? chroma : null;\r\n}\r\n\r\nfunction parseMainKey(keyText) {\r\n const normalizedKeyText = normalizeMusicTheoryText(keyText);\r\n\r\n if (!normalizedKeyText) {\r\n return null;\r\n }\r\n\r\n const match = normalizedKeyText.match(/^([A-Ga-g](?:#|b)?)(m)?$/);\r\n if (!match) {\r\n return null;\r\n }\r\n\r\n const tonic = normalizePitchClass(match[1]);\r\n if (!tonic) {\r\n return null;\r\n }\r\n\r\n const isMinor = match[2] === 'm';\r\n const keyData = isMinor ? getMinorKey(tonic) : getMajorKey(tonic);\r\n const chroma = getPitchClassChroma(keyData.tonic);\r\n\r\n if (!keyData.tonic || chroma == null) {\r\n return null;\r\n }\r\n\r\n return {\r\n raw: `${keyData.tonic}${isMinor ? 'm' : ''}`,\r\n tonic: keyData.tonic,\r\n chroma,\r\n isMinor,\r\n alteration: Number.isFinite(keyData.alteration) ? keyData.alteration : 0,\r\n keySignature: String(keyData.keySignature || ''),\r\n };\r\n}\r\n\r\nfunction normalizeStructureKeyOption(value) {\r\n const parsedKey = parseMainKey(value);\r\n return parsedKey ? parsedKey.raw : normalizeMusicTheoryText(value);\r\n}\r\n\r\nfunction createPitchClassCandidates(chroma) {\r\n const normalizedChroma = ((Number(chroma) % 12) + 12) % 12;\r\n const intervalName = intervalFromSemitones(normalizedChroma);\r\n const flatCandidate = normalizePitchClass(\r\n simplifyNote(transposeNote('C', intervalName)),\r\n );\r\n const sharpCandidate = normalizePitchClass(\r\n getEnharmonicNote(flatCandidate) || flatCandidate,\r\n );\r\n\r\n return {\r\n flatCandidate,\r\n sharpCandidate: sharpCandidate || flatCandidate,\r\n };\r\n}\r\n\r\nfunction choosePreferredPitchClass(chroma, targetKey) {\r\n const { flatCandidate, sharpCandidate } = createPitchClassCandidates(chroma);\r\n\r\n if (!flatCandidate) {\r\n return '';\r\n }\r\n\r\n if (!sharpCandidate || sharpCandidate === flatCandidate) {\r\n return flatCandidate;\r\n }\r\n\r\n if (!targetKey) {\r\n return flatCandidate;\r\n }\r\n\r\n if (targetKey.alteration > 0 || targetKey.raw.includes('#')) {\r\n return sharpCandidate;\r\n }\r\n\r\n if (targetKey.alteration < 0 || targetKey.raw.includes('b')) {\r\n return flatCandidate;\r\n }\r\n\r\n if (!/[#b]/.test(flatCandidate)) {\r\n return flatCandidate;\r\n }\r\n\r\n if (!/[#b]/.test(sharpCandidate)) {\r\n return sharpCandidate;\r\n }\r\n\r\n return flatCandidate;\r\n}\r\n\r\nfunction transposePitchClass(noteText, semitoneOffset, targetKeyText) {\r\n const pitchClass = normalizePitchClass(noteText);\r\n const pitchClassChroma = getPitchClassChroma(pitchClass);\r\n\r\n if (!pitchClass || pitchClassChroma == null) {\r\n return normalizeMusicTheoryText(noteText);\r\n }\r\n\r\n return (\r\n choosePreferredPitchClass(\r\n pitchClassChroma + Number(semitoneOffset),\r\n parseMainKey(targetKeyText),\r\n ) || pitchClass\r\n );\r\n}\r\n\r\nfunction transposeChordSymbol(chordText, semitoneOffset, targetKeyText) {\r\n const normalizedChordText = normalizeChordText(chordText);\r\n\r\n if (!normalizedChordText) {\r\n return normalizedChordText;\r\n }\r\n\r\n const [root = '', suffix = '', bassNote = ''] =\r\n tokenizeChord(normalizedChordText);\r\n\r\n if (!root) {\r\n return normalizedChordText;\r\n }\r\n\r\n const nextRoot = transposePitchClass(root, semitoneOffset, targetKeyText);\r\n const nextBassNote = bassNote\r\n ? transposePitchClass(bassNote, semitoneOffset, targetKeyText)\r\n : '';\r\n\r\n return `${nextRoot}${suffix}${nextBassNote ? `/${nextBassNote}` : ''}`;\r\n}\r\n\r\nfunction simplifyChordSymbol(chordText) {\r\n const normalizedChordText = normalizeChordText(chordText);\r\n\r\n if (!normalizedChordText) {\r\n return normalizedChordText;\r\n }\r\n\r\n const [root = '', rawSuffix = ''] = tokenizeChord(normalizedChordText);\r\n\r\n if (!root) {\r\n return normalizedChordText;\r\n }\r\n\r\n const normalizedSuffixCandidate = normalizeChordSuffixCandidate(rawSuffix);\r\n let simplifiedSuffix = rawSuffix;\r\n\r\n if (normalizedSuffixCandidate === 'minor') {\r\n simplifiedSuffix = 'm';\r\n } else if (\r\n /^m(?:6|7|9|11|13)$/.test(normalizedSuffixCandidate) ||\r\n normalizedSuffixCandidate === 'm7b5' ||\r\n /^mmaj(?:7|9|11|13|7b5)$/.test(normalizedSuffixCandidate)\r\n ) {\r\n simplifiedSuffix = 'm';\r\n }\r\n\r\n return `${root}${simplifiedSuffix}`;\r\n}\r\n\r\n/**\r\n * The seven diatonic triads of a song key, tonic first — the chords a song in\r\n * that key most often reaches for (`E` → `E F#m G#m A B C#m D#dim`). Minor\r\n * keys use the natural minor scale. An unparseable key yields `[]`.\r\n */\r\nfunction getKeyDiatonicTriads(keyText) {\r\n const key = parseMainKey(keyText);\r\n\r\n if (!key) {\r\n return [];\r\n }\r\n\r\n const triads = key.isMinor\r\n ? getMinorKey(key.tonic).natural.triads\r\n : getMajorKey(key.tonic).triads;\r\n\r\n return Array.from(triads || [], (triad) => String(triad || '')).filter(\r\n Boolean,\r\n );\r\n}\r\n\r\nfunction createChordTransposition(sourceKeyText, targetKeyText) {\r\n const sourceKey = parseMainKey(sourceKeyText);\r\n const targetKey = parseMainKey(targetKeyText || sourceKeyText);\r\n\r\n if (!sourceKey || !targetKey) {\r\n return {\r\n isActive: false,\r\n semitoneOffset: 0,\r\n intervalName: intervalFromSemitones(0),\r\n targetKeyText: normalizeMusicTheoryText(targetKeyText || sourceKeyText),\r\n };\r\n }\r\n\r\n const semitoneOffset = (targetKey.chroma - sourceKey.chroma + 12 * 8) % 12;\r\n\r\n return {\r\n isActive: semitoneOffset !== 0 || sourceKey.raw !== targetKey.raw,\r\n semitoneOffset,\r\n intervalName: intervalFromSemitones(semitoneOffset),\r\n targetKeyText: targetKey.raw,\r\n };\r\n}\r\n\r\nfunction selectKeyValueForChroma(keyValues, chroma, isMinor) {\r\n const matchingKeys = (keyValues || [])\r\n .map((value) => parseMainKey(value))\r\n .filter((parsedKey) => {\r\n return (\r\n parsedKey &&\r\n parsedKey.isMinor === isMinor &&\r\n parsedKey.chroma === ((Number(chroma) % 12) + 12) % 12\r\n );\r\n })\r\n .sort((left, right) => {\r\n const alterationDifference =\r\n Math.abs(left.alteration) - Math.abs(right.alteration);\r\n if (alterationDifference) {\r\n return alterationDifference;\r\n }\r\n\r\n const preferredAlterationSign = isMinor ? -1 : 1;\r\n const leftPreferred = left.alteration === preferredAlterationSign;\r\n const rightPreferred = right.alteration === preferredAlterationSign;\r\n if (leftPreferred !== rightPreferred) {\r\n return Number(rightPreferred) - Number(leftPreferred);\r\n }\r\n\r\n return left.raw.localeCompare(right.raw);\r\n });\r\n\r\n return matchingKeys[0]?.raw || '';\r\n}\r\n\r\nfunction getAdjacentChromaticKey(keyText, stepOffset, keyValues) {\r\n const parsedKey = parseMainKey(keyText);\r\n\r\n if (!parsedKey) {\r\n return normalizeStructureKeyOption(keyText);\r\n }\r\n\r\n return (\r\n selectKeyValueForChroma(\r\n keyValues,\r\n parsedKey.chroma + Number(stepOffset),\r\n parsedKey.isMinor,\r\n ) || parsedKey.raw\r\n );\r\n}\r\n\r\nfunction normalizeChordSuffixCandidate(value) {\r\n const normalizedValue = normalizeChordText(value);\r\n\r\n if (!normalizedValue) {\r\n return 'major';\r\n }\r\n\r\n if (normalizedValue === '-') {\r\n return 'minor';\r\n }\r\n\r\n if (/^(?:M|Maj|maj)$/i.test(normalizedValue)) {\r\n return 'major';\r\n }\r\n\r\n if (/^(?:Min|Minor|min|minor)$/i.test(normalizedValue)) {\r\n return 'minor';\r\n }\r\n\r\n const leadingMajorMatch = normalizedValue.match(/^M(.+)$/);\r\n if (leadingMajorMatch) {\r\n return `maj${leadingMajorMatch[1].toLowerCase()}`;\r\n }\r\n\r\n const namedMajorMatch = normalizedValue.match(/^Maj(.+)$/i);\r\n if (namedMajorMatch) {\r\n return `maj${namedMajorMatch[1].toLowerCase()}`;\r\n }\r\n\r\n const namedMinorMatch = normalizedValue.match(/^min(?:or)?(.+)$/i);\r\n if (namedMinorMatch) {\r\n return `m${namedMinorMatch[1].toLowerCase()}`;\r\n }\r\n\r\n const mixedMajorMinorMatch = normalizedValue.match(/^m(?:Maj|M)(.+)$/i);\r\n if (mixedMajorMinorMatch) {\r\n return `mmaj${mixedMajorMinorMatch[1].toLowerCase()}`;\r\n }\r\n\r\n if (/^(?:sus|Sus)$/i.test(normalizedValue)) {\r\n return 'sus4';\r\n }\r\n\r\n if (/^(?:7sus|7Sus)$/i.test(normalizedValue)) {\r\n return '7sus4';\r\n }\r\n\r\n if (/^(?:o|O|°)$/u.test(normalizedValue)) {\r\n return 'dim';\r\n }\r\n\r\n if (/^(?:o7|O7|°7)$/u.test(normalizedValue)) {\r\n return 'dim7';\r\n }\r\n\r\n if (/^(?:ø|ø7|Ø|Ø7)$/u.test(normalizedValue)) {\r\n return 'm7b5';\r\n }\r\n\r\n if (/^\\+$/u.test(normalizedValue) || /^augmented$/i.test(normalizedValue)) {\r\n return 'aug';\r\n }\r\n\r\n const augmentedMatch = normalizedValue.match(/^\\+(.+)$/);\r\n if (augmentedMatch) {\r\n return `aug${augmentedMatch[1].toLowerCase()}`;\r\n }\r\n\r\n if (normalizedValue === '5') {\r\n return 'major';\r\n }\r\n\r\n return normalizedValue.toLowerCase();\r\n}\r\n\r\nfunction extractChordDegreeHint(suffixCandidate) {\r\n const normalizedSuffixCandidate =\r\n normalizeChordSuffixCandidate(suffixCandidate);\r\n\r\n if (!normalizedSuffixCandidate) {\r\n return [];\r\n }\r\n\r\n if (/^(2|4|5|6|7|9|11|13)$/.test(normalizedSuffixCandidate)) {\r\n return [normalizedSuffixCandidate];\r\n }\r\n\r\n const qualityDegreeMatch = normalizedSuffixCandidate.match(\r\n /^(?:maj|m|dim|aug|add|no|omit)(2|4|5|6|7|9|11|13)$/,\r\n );\r\n if (qualityDegreeMatch) {\r\n return [qualityDegreeMatch[1]];\r\n }\r\n\r\n const suspensionMatch = normalizedSuffixCandidate.match(/^sus(2|4)$/);\r\n if (suspensionMatch) {\r\n return [suspensionMatch[1]];\r\n }\r\n\r\n const dominantSuspensionMatch =\r\n normalizedSuffixCandidate.match(/^7sus(2|4)$/);\r\n if (dominantSuspensionMatch) {\r\n return ['7', dominantSuspensionMatch[1]];\r\n }\r\n\r\n if (normalizedSuffixCandidate === 'm7b5') {\r\n return ['7'];\r\n }\r\n\r\n return [];\r\n}\r\n\r\nfunction createDatabaseSuffixCandidates(rawSuffix, chordInfo) {\r\n const candidates = [];\r\n\r\n function push(value) {\r\n const normalizedValue = normalizeChordSuffixCandidate(value);\r\n if (!normalizedValue || candidates.includes(normalizedValue)) {\r\n return;\r\n }\r\n\r\n candidates.push(normalizedValue);\r\n }\r\n\r\n push(rawSuffix);\r\n\r\n if (!normalizeChordText(rawSuffix)) {\r\n push('major');\r\n }\r\n\r\n if (chordInfo && !chordInfo.empty) {\r\n const [, symbolSuffix = ''] = tokenizeChord(chordInfo.symbol || '');\r\n push(symbolSuffix);\r\n chordInfo.aliases.forEach(push);\r\n\r\n if (chordInfo.quality === 'Major' && chordInfo.intervals.length === 3) {\r\n push('major');\r\n }\r\n\r\n if (chordInfo.quality === 'Minor' && chordInfo.intervals.length === 3) {\r\n push('minor');\r\n push('m');\r\n }\r\n\r\n if (chordInfo.quality === 'Augmented') {\r\n push('aug');\r\n }\r\n\r\n if (chordInfo.quality === 'Diminished') {\r\n push('dim');\r\n }\r\n }\r\n\r\n push('major');\r\n\r\n return candidates;\r\n}\r\n\r\nfunction createChordDegreeHints(chordKey) {\r\n const normalizedChordKey = normalizeChordText(chordKey);\r\n const [root = '', rawSuffix = ''] = tokenizeChord(normalizedChordKey);\r\n\r\n if (!root) {\r\n return [];\r\n }\r\n\r\n const chordInfo = getChord(normalizedChordKey);\r\n const hints = [];\r\n\r\n function pushHint(value) {\r\n if (!value || hints.includes(value)) {\r\n return;\r\n }\r\n\r\n hints.push(value);\r\n }\r\n\r\n createDatabaseSuffixCandidates(rawSuffix, chordInfo).forEach((candidate) => {\r\n extractChordDegreeHint(candidate).forEach(pushHint);\r\n });\r\n\r\n if (hints.length || !chordInfo || chordInfo.empty) {\r\n return hints;\r\n }\r\n\r\n chordInfo.intervals.forEach((intervalName) => {\r\n const degreeMatch = String(intervalName).match(/^(2|4|6|7|9|11|13)/);\r\n if (degreeMatch) {\r\n pushHint(degreeMatch[1]);\r\n }\r\n });\r\n\r\n return hints;\r\n}\r\n\r\nfunction createChordDatabaseSearchData(chordKey) {\r\n const normalizedChordKey = normalizeChordText(chordKey);\r\n const [root = '', rawSuffix = '', bassRoot = ''] =\r\n tokenizeChord(normalizedChordKey);\r\n\r\n if (!root) {\r\n return null;\r\n }\r\n\r\n const normalizedRoot = normalizePitchClass(root);\r\n const rootChroma = getPitchClassChroma(normalizedRoot);\r\n\r\n if (!normalizedRoot || rootChroma == null) {\r\n return null;\r\n }\r\n\r\n const normalizedBassRoot = normalizePitchClass(bassRoot);\r\n const bassPitchClass = normalizedBassRoot\r\n ? getPitchClassChroma(normalizedBassRoot)\r\n : null;\r\n const chordInfo = getChord(normalizedChordKey);\r\n\r\n return {\r\n bassPitchClass,\r\n bassRoot: normalizedBassRoot,\r\n chordInfo,\r\n datasetRoot: CHORD_DATABASE_ROOTS_BY_CHROMA[rootChroma] || '',\r\n normalizedChordKey,\r\n rawSuffix,\r\n root: normalizedRoot,\r\n suffixCandidates: createDatabaseSuffixCandidates(rawSuffix, chordInfo),\r\n };\r\n}\r\n\r\nfunction describeChordKey(chordKey) {\r\n const normalizedChordKey = normalizeChordText(chordKey);\r\n\r\n if (!normalizedChordKey) {\r\n return 'Chord';\r\n }\r\n\r\n const chordInfo = getChord(normalizedChordKey);\r\n if (!chordInfo.empty && chordInfo.name) {\r\n return `Chord ${chordInfo.name}`;\r\n }\r\n\r\n return `Chord ${normalizedChordKey}`;\r\n}\r\n\r\nexport {\r\n createChordDatabaseSearchData,\r\n createChordDegreeHints,\r\n createChordTransposition,\r\n describeChordKey,\r\n getAdjacentChromaticKey,\r\n getKeyDiatonicTriads,\r\n normalizeChordText,\r\n normalizeStructureKeyOption,\r\n parseMainKey,\r\n simplifyChordSymbol,\r\n transposeChordSymbol,\r\n transposePitchClass,\r\n intervalSemitones,\r\n};\r\n","import grandPianoUrl from './assets/webaudiofontdata/sound/0000_FluidR3_GM_sf2_file.js?url';\r\nimport electricPianoUrl from './assets/webaudiofontdata/sound/0040_FluidR3_GM_sf2_file.js?url';\r\nimport organUrl from './assets/webaudiofontdata/sound/0170_FluidR3_GM_sf2_file.js?url';\r\nimport nylonGuitarUrl from './assets/webaudiofontdata/sound/0240_FluidR3_GM_sf2_file.js?url';\r\nimport steelGuitarUrl from './assets/webaudiofontdata/sound/0250_FluidR3_GM_sf2_file.js?url';\r\nimport electricGuitarUrl from './assets/webaudiofontdata/sound/0270_FluidR3_GM_sf2_file.js?url';\r\nimport acousticBassUrl from './assets/webaudiofontdata/sound/0320_FluidR3_GM_sf2_file.js?url';\r\nimport electricBassUrl from './assets/webaudiofontdata/sound/0340_FluidR3_GM_sf2_file.js?url';\r\nimport { parseMainKey } from '../OpenLyric/domain/music-theory.ts';\r\nimport {\r\n getChordCardMidi,\r\n resolveChordCard,\r\n type ChordCard,\r\n type ChordDatabases,\r\n} from './chord-library.ts';\r\n\r\n/**\r\n * The strum player's ten instruments: the WebAudioFont soundfont each sounds\r\n * with, and how it voices a chord (`chord-library.ts`). How each is played,\r\n * and the tone the voice engine gives it, is in `instrument-sounds.ts`.\r\n *\r\n * - **Soundfonts** are General MIDI presets of the FluidR3_GM bank (MIT), as\r\n * published in `surikov.github.io/webaudiofontdata` (MIT), picked for a\r\n * worship band: steel-string, nylon and clean electric guitars, grand and\r\n * electric pianos, an organ, nylon guitar again for both ukuleles (General\r\n * MIDI has no ukulele), and an electric and an upright bass. FluidR3 was\r\n * chosen for realism: 22–44.1 kHz samples a few semitones apart, each\r\n * ringing its natural decay for seconds before any loop, and every zone in\r\n * tune within a few cents. The bass is an electric one because the upright\r\n * bass's samples sound almost nothing above 150 Hz: on small speakers its\r\n * line all but disappears. This app serves copies from\r\n * `assets/webaudiofontdata/sound/`, each pinned by `sha384-` hash and\r\n * checked at PACK time against the asset the build emitted, so a copy that\r\n * no longer matches fails the build instead of silently sounding different.\r\n * - **Voicings.** Every instrument voices the song key's chord itself, in\r\n * standard tuning, as its family does (`getVoicingInstrument`): the guitars\r\n * as a guitar, the keyboards as a piano, the basses as a bass. A pattern\r\n * strums that voicing.\r\n */\r\n\r\nexport type StrumInstrumentId =\r\n | 'guitar'\r\n | 'classical-guitar'\r\n | 'electric-guitar'\r\n | 'piano'\r\n | 'electric-piano'\r\n | 'organ'\r\n | 'ukulele'\r\n | 'baritone-ukulele'\r\n | 'bass'\r\n | 'upright-bass';\r\n\r\nexport interface StrumSoundfont {\r\n /** WebAudioFont data file name, e.g. `0250_FluidR3_GM_sf2_file`. */\r\n key: string;\r\n /** The global the data file assigns its preset to. */\r\n variable: string;\r\n /** Where this app serves its copy of the data file. */\r\n url: string;\r\n /**\r\n * `sha384-` hash of the published file, which the copy must match. Checked\r\n * at pack time against the emitted asset (`scripts/pack-packages.ts`), never\r\n * sent as an `integrity` attribute — see `webaudiofont.ts#loadScriptOnce`.\r\n */\r\n integrity: string;\r\n /** General MIDI program and sample bank, for display. */\r\n title: string;\r\n}\r\n\r\nexport interface StrumInstrument {\r\n id: StrumInstrumentId;\r\n label: string;\r\n soundfont: StrumSoundfont;\r\n}\r\n\r\nexport interface StrumVoicing {\r\n /** The key chord that is strummed, e.g. `E` or `C#m`. */\r\n chord: string;\r\n /** MIDI notes in string order (piano: low to high). */\r\n midi: number[];\r\n card: ChordCard;\r\n}\r\n\r\nfunction createSoundfont(\r\n key: string,\r\n title: string,\r\n url: string,\r\n integrity: string,\r\n): StrumSoundfont {\r\n return {\r\n key,\r\n variable: `_tone_${key}`,\r\n url,\r\n integrity,\r\n title,\r\n };\r\n}\r\n\r\nexport const STRUM_SOUNDFONTS = {\r\n steelGuitar: createSoundfont(\r\n '0250_FluidR3_GM_sf2_file',\r\n 'Acoustic Guitar (steel) · FluidR3 bank',\r\n steelGuitarUrl,\r\n 'sha384-JatEI/RMq9LuSL3MPVfxTf3eEwb4NTo0XfOm+H03F8NSxmpchEqNhhikdZqCEGJf',\r\n ),\r\n nylonGuitar: createSoundfont(\r\n '0240_FluidR3_GM_sf2_file',\r\n 'Acoustic Guitar (nylon) · FluidR3 bank',\r\n nylonGuitarUrl,\r\n 'sha384-pB9mj9I09XSMzqCZdNJgeTahU6z6qgIn0Q9gblWMnFhoWxWukgweEiXb5Esf8LyS',\r\n ),\r\n grandPiano: createSoundfont(\r\n '0000_FluidR3_GM_sf2_file',\r\n 'Acoustic Grand Piano · FluidR3 bank',\r\n grandPianoUrl,\r\n 'sha384-Opc1T4lx0TrCrkBPXj88YqbhYJHFpW35+rdWbxLX4xvc04bqnLgRZRpM9mgb4lGS',\r\n ),\r\n electricGuitar: createSoundfont(\r\n '0270_FluidR3_GM_sf2_file',\r\n 'Electric Guitar (clean) · FluidR3 bank',\r\n electricGuitarUrl,\r\n 'sha384-6MXNVE0idSDGHOzxi1U1K27VLNXwh31E8o2EbOMbi9oKUBARlpkilvCU+dkho31k',\r\n ),\r\n electricPiano: createSoundfont(\r\n '0040_FluidR3_GM_sf2_file',\r\n 'Electric Piano · FluidR3 bank',\r\n electricPianoUrl,\r\n 'sha384-rPje2Qq/hTvcSj+/PB8SCpzVJ+oNjIiZEKCPtjVVCEhjfLkfCGr7297Lm4GPB63i',\r\n ),\r\n organ: createSoundfont(\r\n '0170_FluidR3_GM_sf2_file',\r\n 'Percussive Organ · FluidR3 bank',\r\n organUrl,\r\n 'sha384-kijXLjp46P1NvQdW5OxeK1FYG4BZw7Th/ooYFGe3FzKDUPI1zBggVjBtbIvMMiyN',\r\n ),\r\n acousticBass: createSoundfont(\r\n '0320_FluidR3_GM_sf2_file',\r\n 'Acoustic Bass · FluidR3 bank',\r\n acousticBassUrl,\r\n 'sha384-ASEV4mQ6SA2eqvc6C0LEBfUbtP9ou7oten1II4ZoixNqwVfC+ONppDDykMphvNx+',\r\n ),\r\n electricBass: createSoundfont(\r\n '0340_FluidR3_GM_sf2_file',\r\n 'Electric Bass (pick) · FluidR3 bank',\r\n electricBassUrl,\r\n 'sha384-6bReDDQWbusyjn9xmyV23fkyimxADiQ6RkjfETqL6ZL57mJNsImJJm+4XLfP1WVy',\r\n ),\r\n} as const;\r\n\r\nexport const STRUM_INSTRUMENTS: readonly StrumInstrument[] = [\r\n {\r\n id: 'guitar',\r\n label: 'Acoustic Guitar',\r\n soundfont: STRUM_SOUNDFONTS.steelGuitar,\r\n },\r\n {\r\n id: 'classical-guitar',\r\n label: 'Classical Guitar',\r\n soundfont: STRUM_SOUNDFONTS.nylonGuitar,\r\n },\r\n {\r\n id: 'electric-guitar',\r\n label: 'Electric Guitar',\r\n soundfont: STRUM_SOUNDFONTS.electricGuitar,\r\n },\r\n { id: 'piano', label: 'Piano', soundfont: STRUM_SOUNDFONTS.grandPiano },\r\n {\r\n id: 'electric-piano',\r\n label: 'Electric Piano',\r\n soundfont: STRUM_SOUNDFONTS.electricPiano,\r\n },\r\n { id: 'organ', label: 'Organ', soundfont: STRUM_SOUNDFONTS.organ },\r\n {\r\n id: 'ukulele',\r\n label: 'Ukulele (GCEA)',\r\n soundfont: STRUM_SOUNDFONTS.nylonGuitar,\r\n },\r\n {\r\n id: 'baritone-ukulele',\r\n label: 'Baritone Ukulele (DGBE)',\r\n soundfont: STRUM_SOUNDFONTS.nylonGuitar,\r\n },\r\n {\r\n id: 'bass',\r\n label: 'Bass (EADG)',\r\n soundfont: STRUM_SOUNDFONTS.electricBass,\r\n },\r\n {\r\n id: 'upright-bass',\r\n label: 'Upright Bass (EADG)',\r\n soundfont: STRUM_SOUNDFONTS.acousticBass,\r\n },\r\n];\r\n\r\nconst SHARP_NAMES = [\r\n 'C',\r\n 'C#',\r\n 'D',\r\n 'D#',\r\n 'E',\r\n 'F',\r\n 'F#',\r\n 'G',\r\n 'G#',\r\n 'A',\r\n 'A#',\r\n 'B',\r\n];\r\nconst FLAT_NAMES = [\r\n 'C',\r\n 'Db',\r\n 'D',\r\n 'Eb',\r\n 'E',\r\n 'F',\r\n 'Gb',\r\n 'G',\r\n 'Ab',\r\n 'A',\r\n 'Bb',\r\n 'B',\r\n];\r\n\r\nexport function getStrumInstrument(id: string): StrumInstrument | null {\r\n return STRUM_INSTRUMENTS.find((instrument) => instrument.id === id) ?? null;\r\n}\r\n\r\n/**\r\n * The chord a pattern strums on `instrumentId` in `keyText` — the key's\r\n * tonic chord (`E`, `C#m`) — or `null` when the key is unreadable or the\r\n * instrument has no voicing for it.\r\n */\r\nexport function resolveStrumVoicing(\r\n instrumentId: StrumInstrumentId,\r\n keyText: string,\r\n databases: ChordDatabases,\r\n): StrumVoicing | null {\r\n const key = parseMainKey(keyText);\r\n\r\n if (!key) {\r\n return null;\r\n }\r\n\r\n const card = resolveChordCard(instrumentId, key.raw, databases);\r\n const midi = getChordCardMidi(card);\r\n\r\n return midi.length ? { chord: key.raw, midi, card } : null;\r\n}\r\n\r\n/** `56` → `G#3`, or `Ab3` in a key written with flats. */\r\nexport function getMidiNoteName(midi: number, keyText = ''): string {\r\n const names =\r\n (parseMainKey(keyText)?.alteration ?? 0) < 0 ? FLAT_NAMES : SHARP_NAMES;\r\n\r\n return `${names[((midi % 12) + 12) % 12]}${Math.floor(midi / 12) - 1}`;\r\n}\r\n","import {\r\n parseStrummingPatterns,\r\n parseStrummingPatternSteps,\r\n} from '../OpenLyric/preview/helpers.ts';\r\nimport { isValidStrummingPatternValue } from '../../editor/scripts/shared.ts';\r\n\r\n/**\r\n * Strum patterns in Open Lyric notation — the `Strumming Patterns` Config\r\n * field (`d--u-- | d-u---`), parsed by the notation's own parser\r\n * (`preview/helpers.ts`). One character is one sixteenth-note step; the\r\n * notation carries direction only, so each\r\n * stroke's loudness comes from where it falls in the bar\r\n * ({@link getStepVelocity}).\r\n */\r\n\r\nexport type StrumDirection = 'down' | 'up';\r\n\r\nexport interface StrumStep {\r\n direction: StrumDirection;\r\n /** Loudness, 0–1. */\r\n velocity: number;\r\n}\r\n\r\n/** What a page keeps and persists: the notation text is the source of truth. */\r\nexport interface StrumPatternDefinition {\r\n id: string;\r\n label: string;\r\n source: string;\r\n}\r\n\r\nexport interface StrumPattern extends StrumPatternDefinition {\r\n /** Step `i` is the `i`-th character of the grid; `null` = rest. */\r\n steps: (StrumStep | null)[];\r\n /** The count printed under each step. */\r\n counts: string[];\r\n /** 0-based steps a `|` or space separator precedes. */\r\n groupBreaks: number[];\r\n}\r\n\r\n/** Loudness by position: bar downbeat, other beats, off-beats, the rest. */\r\nexport const STEP_VELOCITIES = {\r\n downbeat: 0.9,\r\n beat: 0.75,\r\n offbeat: 0.65,\r\n subdivision: 0.55,\r\n} as const;\r\n\r\nconst EIGHTH_COUNTED_TIMES = new Set(['3/8', '6/8', '9/8', '12/8']);\r\nconst STRUMMING_PATTERNS_HEADER_RE = /^\\s*-?\\s*Strumming Patterns?\\s*:\\s*/i;\r\nconst OPEN_LYRIC_REST = '-';\r\n\r\nexport class StrumPatternError extends Error {}\r\n\r\nexport function getTimeSignatureParts(timeSignature: string): {\r\n beats: number;\r\n unit: number;\r\n} {\r\n const [beats, unit] = String(timeSignature ?? '')\r\n .split('/')\r\n .map((part) => Number.parseInt(part, 10));\r\n\r\n return {\r\n beats: Number.isFinite(beats) && beats > 0 ? beats : 4,\r\n unit: Number.isFinite(unit) && unit > 0 ? unit : 4,\r\n };\r\n}\r\n\r\nfunction isEighthCounted(timeSignature: string) {\r\n return EIGHTH_COUNTED_TIMES.has(String(timeSignature).trim());\r\n}\r\n\r\n/**\r\n * Seconds from one step to the next. The tempo counts the time signature's\r\n * unit, and a step is always a sixteenth note: `unit / 16` of a beat.\r\n */\r\nexport function getStrumStepSeconds(tempo: number, timeSignature: string) {\r\n const { unit } = getTimeSignatureParts(timeSignature);\r\n\r\n return (60 / tempo) * (unit / 16);\r\n}\r\n\r\n/**\r\n * The count under step `index`, as the notation's preview prints it\r\n * (`getStrummingStepLabel` in `preview/helpers.ts`): `1 e & a` per beat, or\r\n * `1 &` per eighth in the eighth-counted times.\r\n */\r\nexport function getOpenLyricStepCount(\r\n index: number,\r\n timeSignature: string,\r\n): string {\r\n if (isEighthCounted(timeSignature)) {\r\n return index % 2 === 0 ? String(Math.floor(index / 2) + 1) : '&';\r\n }\r\n\r\n const phase = index % 4;\r\n\r\n return phase === 0\r\n ? String(Math.floor(index / 4) + 1)\r\n : ['', 'e', '&', 'a'][phase];\r\n}\r\n\r\n/**\r\n * How loud a stroke on step `index` is. Sixteenth grid: the bar's first beat\r\n * is loudest, then the other beats, the `&`s, and the `e`/`a` sixteenths.\r\n * Eighth-counted times (steps counted `1 & 2 & …`, one number per eighth)\r\n * accent each dotted quarter — every third eighth, so every sixth step — then\r\n * the other eighths, and the `&` sixteenths between them the softest.\r\n */\r\nexport function getStepVelocity(index: number, timeSignature: string): number {\r\n const { beats } = getTimeSignatureParts(timeSignature);\r\n\r\n if (isEighthCounted(timeSignature)) {\r\n const barSteps = beats * 2;\r\n const inBar = index % barSteps;\r\n\r\n if (inBar === 0) {\r\n return STEP_VELOCITIES.downbeat;\r\n }\r\n\r\n if (inBar % 6 === 0) {\r\n return STEP_VELOCITIES.beat;\r\n }\r\n\r\n return inBar % 2 === 0\r\n ? STEP_VELOCITIES.offbeat\r\n : STEP_VELOCITIES.subdivision;\r\n }\r\n\r\n const inBar = index % (beats * 4);\r\n\r\n if (inBar === 0) {\r\n return STEP_VELOCITIES.downbeat;\r\n }\r\n\r\n switch (inBar % 4) {\r\n case 0:\r\n return STEP_VELOCITIES.beat;\r\n case 2:\r\n return STEP_VELOCITIES.offbeat;\r\n default:\r\n return STEP_VELOCITIES.subdivision;\r\n }\r\n}\r\n\r\n/** One Open Lyric pattern line, e.g. `d--u-- | d-u---`. */\r\nexport function parseOpenLyricPattern(\r\n definition: StrumPatternDefinition,\r\n timeSignature: string,\r\n): StrumPattern {\r\n if (!isValidStrummingPatternValue(definition.source)) {\r\n throw new StrumPatternError(\r\n `\"${definition.source}\" is not an Open Lyric strumming pattern. Use d/D/v/V (down), u/U/^ (up), x/X/1/*/+ (alternating), -/_/./0 (rest) and | or spaces between groups.`,\r\n );\r\n }\r\n\r\n const parsedSteps = parseStrummingPatternSteps(definition.source);\r\n\r\n return {\r\n ...definition,\r\n steps: parsedSteps.map((step, index): StrumStep | null =>\r\n step.direction === 'down' || step.direction === 'up'\r\n ? {\r\n direction: step.direction,\r\n velocity: getStepVelocity(index, timeSignature),\r\n }\r\n : null,\r\n ),\r\n counts: parsedSteps.map((_, index) =>\r\n getOpenLyricStepCount(index, timeSignature),\r\n ),\r\n groupBreaks: parsedSteps\r\n .filter((step) => step.breakBefore)\r\n .map((step) => step.index),\r\n };\r\n}\r\n\r\n/**\r\n * Patterns typed or pasted in: the body of a `Strumming Patterns` field,\r\n * `// label` lines included, its `- Strumming Patterns:` header optional.\r\n */\r\nexport function readStrumPatternInput(\r\n text: string,\r\n createId: () => string,\r\n nextLabel: () => string,\r\n): StrumPatternDefinition[] {\r\n const trimmed = String(text ?? '').trim();\r\n\r\n if (!trimmed) {\r\n throw new StrumPatternError('Type or paste a pattern first.');\r\n }\r\n\r\n const lines = trimmed\r\n .split(/\\r?\\n/)\r\n .map((line) => line.replace(STRUMMING_PATTERNS_HEADER_RE, ''));\r\n const patterns = parseStrummingPatterns(lines);\r\n\r\n if (!patterns.length) {\r\n throw new StrumPatternError('No pattern found — only comment lines.');\r\n }\r\n\r\n return patterns.map((pattern) => {\r\n const definition: StrumPatternDefinition = {\r\n id: createId(),\r\n label: pattern.labelSourceIndex == null ? nextLabel() : pattern.label,\r\n source: pattern.pattern,\r\n };\r\n\r\n parseOpenLyricPattern(definition, '4/4');\r\n return definition;\r\n });\r\n}\r\n\r\n/** The definition with step `index` cycled rest → down → up → rest. */\r\nexport function cycleStrumPatternStep(\r\n definition: StrumPatternDefinition,\r\n index: number,\r\n timeSignature: string,\r\n): StrumPatternDefinition {\r\n const pattern = parseOpenLyricPattern(definition, timeSignature);\r\n const parsedStep = parseStrummingPatternSteps(definition.source)[index];\r\n\r\n if (!parsedStep) {\r\n return definition;\r\n }\r\n\r\n const direction = pattern.steps[index]?.direction;\r\n const nextCharacter =\r\n direction === 'down' ? 'u' : direction === 'up' ? OPEN_LYRIC_REST : 'd';\r\n\r\n return {\r\n ...definition,\r\n source: `${definition.source.slice(0, parsedStep.sourceOffset)}${nextCharacter}${definition.source.slice(parsedStep.sourceOffset + 1)}`,\r\n };\r\n}\r\n","import type { StrumInstrumentId } from './instruments.ts';\r\n\r\n/**\r\n * How each of the strum player's instruments is played, and how it sounds —\r\n * the performance and tone the voice engine (`StrumSoundEngine.ts`) gives the\r\n * soundfont samples, so a pattern sounds like a player on a real instrument\r\n * rather than a sampler repeating itself:\r\n *\r\n * - **Technique.** The guitars and both ukuleles strum across their strings\r\n * in string order, so the re-entrant ukulele's high G sounds first on a\r\n * down stroke. A piano, electric piano or organ strikes the voicing as one\r\n * chord, the right hand alone on an up stroke. A bass plucks one note a\r\n * stroke.\r\n * - **Touch.** How fast a stroke crosses the strings, which strings an up\r\n * stroke catches, how much a stroke's force changes the sound\r\n * (`dynamics` — an organ's keys barely do), and how far a player's timing,\r\n * force and tuning wander from stroke to stroke (`humanize`).\r\n * - **Tone.** A soft stroke sounds darker than a hard one (`tone`), as on the\r\n * real instrument. The `body` filter takes a small instrument's low end away\r\n * — a ukulele sounds from nylon-guitar samples — and a bass's `drive` adds\r\n * the harmonics an amplifier gives it, which carry its line on small\r\n * speakers that cannot play its low fundamentals. The room is a short, warm\r\n * reverb.\r\n * - **Ring.** A string (a key) rings until it is struck again, damped, or\r\n * dies away. `sustain` shortens a small body's ring; `tail` fades a sample\r\n * once it runs into its loop (an organ holds its note); `damping` is how\r\n * fast a re-struck string lets go of its old note; `release` is a stop, the\r\n * player's palm (the piano's dampers) settling on the strings.\r\n *\r\n * Every time here is in seconds; a time constant is the time a sound takes to\r\n * fall to about a third (−8.7 dB).\r\n */\r\n\r\nexport type StrumTechnique = 'strum' | 'keys' | 'pluck';\r\n\r\nexport interface StrumSound {\r\n technique: StrumTechnique;\r\n /**\r\n * Seconds between neighbouring notes of a stroke at full force: a strum's\r\n * crossing of the strings, or how loosely a keyboard's chord lands.\r\n */\r\n noteGap: number;\r\n /** Share of the sounding strings an up stroke catches, from the highest. */\r\n upShare: number;\r\n /** Seconds between the notes of a chord card's roll. */\r\n rollGap: number;\r\n /** Low-pass cutoff (Hz), at middle C, of the softest and the hardest stroke. */\r\n tone: { soft: number; hard: number };\r\n /** How much a stroke's force changes its loudness: 1 fully, 0 not at all. */\r\n dynamics: number;\r\n /** Time constant of the body's decay on top of the sample's own; 0: none. */\r\n sustain: number;\r\n /** Time constant of the fade once a sample runs into its sustain loop. */\r\n tail: number;\r\n /** Longest a note rings. */\r\n maxRing: number;\r\n /** Time constant a re-struck string or key lets go of its old note with. */\r\n damping: number;\r\n /** Time constant of a stop: the palm or the dampers on the strings. */\r\n release: number;\r\n /** Pan of the outermost strings or keys, each side of centre (0–1). */\r\n width: number;\r\n /**\r\n * The body: a high-pass (Hz), an optional presence peak (Hz, dB), and an\r\n * optional `drive` — the share of amp-like harmonics mixed in (0–1).\r\n */\r\n body: {\r\n highpass: number;\r\n presence?: { frequency: number; gain: number };\r\n drive?: number;\r\n };\r\n /** Share of the sound sent to the room reverb (0–1). */\r\n room: number;\r\n /**\r\n * Output gain, balancing the instruments against each other: measured so a\r\n * pattern on each plays at about the same loudness (−13 LUFS).\r\n */\r\n level: number;\r\n /**\r\n * How far a player wanders, at most, each way: a stroke's timing\r\n * (seconds), a note's force (a share of it) and tuning (cents).\r\n */\r\n humanize: { timing: number; velocity: number; detune: number };\r\n}\r\n\r\nexport const STRUM_SOUNDS: Readonly<Record<StrumInstrumentId, StrumSound>> = {\r\n guitar: {\r\n technique: 'strum',\r\n noteGap: 0.0055,\r\n upShare: 0.6,\r\n rollGap: 0.045,\r\n tone: { soft: 4500, hard: 16000 },\r\n dynamics: 1,\r\n sustain: 0,\r\n tail: 0.6,\r\n maxRing: 7,\r\n damping: 0.012,\r\n release: 0.18,\r\n width: 0.25,\r\n body: { highpass: 70 },\r\n room: 0.2,\r\n level: 0.88,\r\n humanize: { timing: 0.004, velocity: 0.06, detune: 3 },\r\n },\r\n 'classical-guitar': {\r\n technique: 'strum',\r\n noteGap: 0.0065,\r\n upShare: 0.6,\r\n rollGap: 0.05,\r\n tone: { soft: 3500, hard: 11000 },\r\n dynamics: 1,\r\n sustain: 0,\r\n tail: 0.6,\r\n maxRing: 6,\r\n damping: 0.015,\r\n release: 0.2,\r\n width: 0.25,\r\n body: { highpass: 70 },\r\n room: 0.22,\r\n level: 0.88,\r\n humanize: { timing: 0.005, velocity: 0.07, detune: 3 },\r\n },\r\n 'electric-guitar': {\r\n technique: 'strum',\r\n noteGap: 0.005,\r\n upShare: 0.6,\r\n rollGap: 0.045,\r\n tone: { soft: 4000, hard: 12000 },\r\n dynamics: 1,\r\n sustain: 0,\r\n tail: 1,\r\n maxRing: 8,\r\n damping: 0.01,\r\n release: 0.15,\r\n width: 0.3,\r\n body: { highpass: 90 },\r\n room: 0.32,\r\n level: 0.76,\r\n humanize: { timing: 0.004, velocity: 0.06, detune: 2 },\r\n },\r\n piano: {\r\n technique: 'keys',\r\n noteGap: 0.003,\r\n upShare: 1,\r\n rollGap: 0.035,\r\n tone: { soft: 3000, hard: 14000 },\r\n dynamics: 1,\r\n sustain: 0,\r\n tail: 1.5,\r\n maxRing: 12,\r\n damping: 0.05,\r\n release: 0.25,\r\n width: 0.35,\r\n body: { highpass: 30 },\r\n room: 0.24,\r\n level: 1.22,\r\n humanize: { timing: 0.003, velocity: 0.05, detune: 1.5 },\r\n },\r\n 'electric-piano': {\r\n technique: 'keys',\r\n noteGap: 0.003,\r\n upShare: 1,\r\n rollGap: 0.035,\r\n tone: { soft: 2000, hard: 8000 },\r\n dynamics: 1,\r\n sustain: 3.5,\r\n tail: 1,\r\n maxRing: 10,\r\n damping: 0.05,\r\n release: 0.15,\r\n width: 0.45,\r\n body: { highpass: 40 },\r\n room: 0.22,\r\n level: 0.73,\r\n humanize: { timing: 0.003, velocity: 0.05, detune: 1.5 },\r\n },\r\n organ: {\r\n technique: 'keys',\r\n noteGap: 0.002,\r\n upShare: 1,\r\n rollGap: 0.035,\r\n tone: { soft: 12000, hard: 12000 },\r\n dynamics: 0.25,\r\n sustain: 0,\r\n tail: 60,\r\n maxRing: 4,\r\n damping: 0.02,\r\n release: 0.06,\r\n width: 0.3,\r\n body: { highpass: 40 },\r\n room: 0.28,\r\n level: 0.62,\r\n humanize: { timing: 0.003, velocity: 0, detune: 0 },\r\n },\r\n ukulele: {\r\n technique: 'strum',\r\n noteGap: 0.005,\r\n upShare: 1,\r\n rollGap: 0.04,\r\n tone: { soft: 5000, hard: 14000 },\r\n dynamics: 1,\r\n sustain: 2,\r\n tail: 0.6,\r\n maxRing: 3,\r\n damping: 0.012,\r\n release: 0.12,\r\n width: 0.15,\r\n body: { highpass: 200, presence: { frequency: 2200, gain: 2 } },\r\n room: 0.18,\r\n level: 0.97,\r\n humanize: { timing: 0.004, velocity: 0.07, detune: 4 },\r\n },\r\n 'baritone-ukulele': {\r\n technique: 'strum',\r\n noteGap: 0.0055,\r\n upShare: 0.75,\r\n rollGap: 0.045,\r\n tone: { soft: 3500, hard: 12000 },\r\n dynamics: 1,\r\n sustain: 1.4,\r\n tail: 0.45,\r\n maxRing: 4.5,\r\n damping: 0.012,\r\n release: 0.15,\r\n width: 0.2,\r\n body: { highpass: 110, presence: { frequency: 1800, gain: 1 } },\r\n room: 0.18,\r\n level: 1.11,\r\n humanize: { timing: 0.004, velocity: 0.06, detune: 3 },\r\n },\r\n bass: {\r\n technique: 'pluck',\r\n noteGap: 0,\r\n upShare: 1,\r\n rollGap: 0.18,\r\n tone: { soft: 2500, hard: 9000 },\r\n dynamics: 1,\r\n sustain: 2.2,\r\n tail: 0.6,\r\n maxRing: 5,\r\n damping: 0.02,\r\n release: 0.08,\r\n width: 0,\r\n body: { highpass: 35, drive: 0.45 },\r\n room: 0.05,\r\n level: 1.1,\r\n humanize: { timing: 0.004, velocity: 0.05, detune: 2 },\r\n },\r\n 'upright-bass': {\r\n technique: 'pluck',\r\n noteGap: 0,\r\n upShare: 1,\r\n rollGap: 0.2,\r\n tone: { soft: 1800, hard: 6000 },\r\n dynamics: 1,\r\n sustain: 1.4,\r\n tail: 0.5,\r\n maxRing: 4,\r\n damping: 0.03,\r\n release: 0.1,\r\n width: 0,\r\n body: { highpass: 35, drive: 0.7 },\r\n room: 0.08,\r\n level: 1.05,\r\n humanize: { timing: 0.004, velocity: 0.05, detune: 3 },\r\n },\r\n};\r\n\r\nexport function getStrumSound(instrument: StrumInstrumentId): StrumSound {\r\n return STRUM_SOUNDS[instrument];\r\n}\r\n","import type { StrumSound } from './instrument-sounds.ts';\r\nimport type { StrumDirection, StrumStep } from './patterns.ts';\r\n\r\n/**\r\n * How one step of a pattern becomes the notes a player sounds, and how a\r\n * chord card rolls its chord — the performance, before any sound is made\r\n * (`StrumSoundEngine.ts` plays it). Each instrument plays its own way\r\n * (`technique` in `instrument-sounds.ts`):\r\n *\r\n * - **Strum** (guitar, ukuleles). A down stroke crosses the strings in string\r\n * order, lowest string first; an up stroke comes back from the highest and\r\n * catches only the top `upShare` of them. A light down stroke — on an `e` or\r\n * `a` sixteenth — skips a guitar's lowest string. Each string sounds\r\n * `noteGap` after the one before: a little slower on a soft stroke, quicker\r\n * on an up stroke. The strings the pick meets last sound a little softer.\r\n * - **Keys** (piano). A down stroke strikes the whole voicing, an up stroke\r\n * the voicing without its lowest note — the right hand alone — a little\r\n * softer. The notes land within a few milliseconds of each other.\r\n * - **Pluck** (bass). A down stroke plucks the voicing's lowest note, its\r\n * root; an up stroke its highest, the octave. A bass sounds one note at a\r\n * time.\r\n *\r\n * A stroke's force is its step's velocity (`getStepVelocity`). A player is\r\n * human: each stroke lands up to `humanize.timing` off the grid, and each note\r\n * wanders up to `humanize.velocity` in force and `humanize.detune` in tuning.\r\n * `random` draws those; one that always returns `0.5` plays exactly.\r\n */\r\n\r\nexport interface PlannedNote {\r\n pitch: number;\r\n /** Seconds after the stroke lands — after the roll starts, for a roll. */\r\n offset: number;\r\n /** Loudness, 0–1. */\r\n velocity: number;\r\n /**\r\n * The string (a piano's key) that sounds it — its index in the voicing. A\r\n * new note on a string damps the one ringing there; a bass has one.\r\n */\r\n slot: number;\r\n /** Cents off pitch. */\r\n detune: number;\r\n /** Stereo position, −1 (left) to 1 (right). */\r\n pan: number;\r\n}\r\n\r\nexport interface StrokePlan {\r\n direction: StrumDirection;\r\n /** Seconds the stroke lands after its step; negative when early. */\r\n shift: number;\r\n /** The notes in the order the stroke meets them. */\r\n notes: PlannedNote[];\r\n}\r\n\r\n/** How loud a chord card's roll is. */\r\nexport const CHORD_ROLL_VELOCITY = 0.7;\r\n/** A down stroke this soft or softer skips the lowest string. */\r\nconst LIGHT_STROKE_VELOCITY = 0.6;\r\n/** The fewest strings a stroke skipping the lowest one leaves sounding. */\r\nconst LIGHT_STROKE_MIN_STRINGS = 4;\r\n/** An up stroke crosses the strings this much faster than a down stroke. */\r\nconst UP_STROKE_SPEED = 0.85;\r\n/** How much softer the last string a stroke meets is than the first. */\r\nconst DOWN_STROKE_TAPER = 0.12;\r\nconst UP_STROKE_TAPER = 0.25;\r\n/** A piano's right hand alone, on an up stroke. */\r\nconst RIGHT_HAND_FORCE = 0.9;\r\n/** How far each gap between strings, or roll notes, wanders (a share). */\r\nconst GAP_WANDER = 0.2;\r\n/** Where a piano's pan is centred, and the notes either side reaching full width. */\r\nconst PIANO_PAN_CENTRE = 60;\r\nconst PIANO_PAN_SPAN = 30;\r\nconst QUIETEST_NOTE = 0.05;\r\n\r\nfunction clamp(value: number, low: number, high: number) {\r\n return Math.min(high, Math.max(low, value));\r\n}\r\n\r\nfunction range(from: number, to: number) {\r\n return Array.from({ length: Math.max(0, to - from) }, (_, i) => from + i);\r\n}\r\n\r\n/** A draw between `-amount` and `amount`. */\r\nfunction wander(random: () => number, amount: number) {\r\n return (random() * 2 - 1) * amount;\r\n}\r\n\r\n/**\r\n * The voicing's notes a stroke sounds, as indexes into it, in the order the\r\n * stroke meets them.\r\n */\r\nexport function getStrokeStrings(\r\n count: number,\r\n direction: StrumDirection,\r\n velocity: number,\r\n sound: StrumSound,\r\n random: () => number,\r\n): number[] {\r\n if (!count) {\r\n return [];\r\n }\r\n\r\n switch (sound.technique) {\r\n case 'keys':\r\n return direction === 'up' && count >= 3\r\n ? range(1, count)\r\n : range(0, count);\r\n case 'pluck':\r\n return [direction === 'up' ? count - 1 : 0];\r\n default: {\r\n if (direction === 'down') {\r\n const skip =\r\n velocity <= LIGHT_STROKE_VELOCITY && count > LIGHT_STROKE_MIN_STRINGS\r\n ? 1\r\n : 0;\r\n\r\n return range(skip, count);\r\n }\r\n\r\n const caught = clamp(\r\n Math.round(count * sound.upShare + (random() - 0.5)),\r\n Math.min(2, count),\r\n count,\r\n );\r\n\r\n return range(count - caught, count).reverse();\r\n }\r\n }\r\n}\r\n\r\nfunction getPan(sound: StrumSound, slot: number, count: number, pitch: number) {\r\n switch (sound.technique) {\r\n case 'keys':\r\n return (\r\n clamp((pitch - PIANO_PAN_CENTRE) / PIANO_PAN_SPAN, -1, 1) * sound.width\r\n );\r\n case 'pluck':\r\n return 0;\r\n default:\r\n return count > 1 ? sound.width * ((2 * slot) / (count - 1) - 1) : 0;\r\n }\r\n}\r\n\r\n/** The notes of step `step`'s stroke, or `null` for a rest or no chord. */\r\nexport function planStroke(\r\n chord: readonly number[],\r\n step: StrumStep | null | undefined,\r\n sound: StrumSound,\r\n random: () => number = Math.random,\r\n): StrokePlan | null {\r\n if (!step || !chord.length) {\r\n return null;\r\n }\r\n\r\n const force = step.velocity;\r\n const up = step.direction === 'up';\r\n const strings = getStrokeStrings(\r\n chord.length,\r\n step.direction,\r\n force,\r\n sound,\r\n random,\r\n );\r\n const speed = (1.35 - 0.5 * force) * (up ? UP_STROKE_SPEED : 1);\r\n const taper = up ? UP_STROKE_TAPER : DOWN_STROKE_TAPER;\r\n const rightHand = up && strings.length < chord.length ? RIGHT_HAND_FORCE : 1;\r\n let offset = 0;\r\n\r\n const notes = strings.map((index, order): PlannedNote => {\r\n if (order > 0) {\r\n offset +=\r\n sound.technique === 'keys'\r\n ? 0\r\n : sound.noteGap * speed * (1 + wander(random, GAP_WANDER));\r\n }\r\n\r\n const pitch = chord[index];\r\n const reach = strings.length > 1 ? order / (strings.length - 1) : 0;\r\n let velocity: number;\r\n let noteOffset = offset;\r\n\r\n switch (sound.technique) {\r\n case 'keys':\r\n velocity = force * rightHand;\r\n noteOffset = order > 0 ? random() * 2 * sound.noteGap : 0;\r\n break;\r\n case 'pluck':\r\n velocity = force;\r\n break;\r\n default:\r\n velocity = force * (1 - taper * reach);\r\n }\r\n\r\n return {\r\n pitch,\r\n offset: noteOffset,\r\n velocity: clamp(\r\n velocity * (1 + wander(random, sound.humanize.velocity)),\r\n QUIETEST_NOTE,\r\n 1,\r\n ),\r\n slot: sound.technique === 'pluck' ? 0 : index,\r\n detune: wander(random, sound.humanize.detune),\r\n pan: getPan(sound, index, chord.length, pitch),\r\n };\r\n });\r\n\r\n return {\r\n direction: step.direction,\r\n shift: wander(random, sound.humanize.timing),\r\n notes,\r\n };\r\n}\r\n\r\n/**\r\n * A chord card's play: the notes one after another in the order given — from\r\n * the first string listed — `rollGap` apart, each on its own string.\r\n */\r\nexport function planChordRoll(\r\n midi: readonly number[],\r\n sound: StrumSound,\r\n random: () => number = Math.random,\r\n): PlannedNote[] {\r\n let offset = 0;\r\n\r\n return midi.map((pitch, index) => {\r\n if (index > 0) {\r\n offset += sound.rollGap * (1 + wander(random, GAP_WANDER));\r\n }\r\n\r\n return {\r\n pitch,\r\n offset,\r\n velocity: clamp(\r\n CHORD_ROLL_VELOCITY * (1 + wander(random, sound.humanize.velocity)),\r\n QUIETEST_NOTE,\r\n 1,\r\n ),\r\n slot: index,\r\n detune: wander(random, sound.humanize.detune),\r\n pan: getPan(sound, index, midi.length, pitch),\r\n };\r\n });\r\n}\r\n\r\n/** Seconds from a roll's first note to its last, before any wandering. */\r\nexport function getChordRollSeconds(sound: StrumSound, noteCount: number) {\r\n return Math.max(0, noteCount - 1) * sound.rollGap;\r\n}\r\n","import type { StrumSound } from './instrument-sounds.ts';\r\nimport type { WebAudioFontPreset, WebAudioFontZone } from './webaudiofont.ts';\r\n\r\n/**\r\n * The strum player's voice engine: plays soundfont samples the way a real\r\n * instrument sounds them, on the audio clock. It is this repo's own MIT code;\r\n * WebAudioFont only loads and decodes the samples (`webaudiofont.ts`).\r\n *\r\n * Each note is its sample, pitched from the soundfont zone nearest its key,\r\n * through a chain of its own:\r\n *\r\n * - **Tone** — a low-pass that opens with force and pitch, since a soft stroke\r\n * is darker than a hard one and a high string brighter than a low one.\r\n * - **Envelope** — the sample's own attack and decay. A body's `sustain`\r\n * shortens the ring, the sample's loop fades out over `tail` instead of\r\n * droning, and no note outlasts `maxRing`. Each zone's level is evened out\r\n * against the others, so no string of a chord jumps out, and each sample\r\n * starts where it becomes audible — some carry milliseconds of silence\r\n * before the pluck, which would make every note late.\r\n * - **Damper** — a string (`slot`) sounds one note: a new note there lets go\r\n * of the old one over `damping`, while the strings it does not touch ring\r\n * on. `damp` settles a whole group, as a palm on the strings.\r\n * - **Pan** — each string or key has its place across the stereo field.\r\n *\r\n * The notes of one instrument meet on its bus: the body (a high-pass, a\r\n * presence peak for a small instrument, and a bass's drive — soft saturation\r\n * band-limited to the harmonics a small speaker can play, mixed in under the\r\n * dry sound as an amplifier's would be), then the dry mix and a send into the\r\n * room — a convolution reverb whose impulse is synthesised here (a warm,\r\n * 1.3 s room), so no file is loaded for it. A peak limiter keeps loud strums\r\n * from clipping without flattening them.\r\n */\r\n\r\nexport interface StrumNote {\r\n preset: WebAudioFontPreset;\r\n sound: StrumSound;\r\n /** MIDI note. */\r\n pitch: number;\r\n /** Audio-clock time it sounds. */\r\n time: number;\r\n /** Loudness, 0–1. */\r\n velocity: number;\r\n /** Cents off pitch. */\r\n detune?: number;\r\n /** −1 (left) to 1 (right). */\r\n pan?: number;\r\n /** The string or key it sounds on: a new note there damps this one. */\r\n slot: string;\r\n /** The notes `damp` settles together, e.g. a pattern's or a chord card's. */\r\n group: string;\r\n}\r\n\r\nexport interface StrumVoice {\r\n /** Audio-clock time the note sounds. */\r\n readonly time: number;\r\n /** Silences the note if it has not sounded yet; a sounding note rings on. */\r\n cancel(): void;\r\n}\r\n\r\nexport interface StrumSoundEngine {\r\n /** Schedules a note; `null` when the soundfont has no sample for it. */\r\n play(note: StrumNote): StrumVoice | null;\r\n /**\r\n * Lets go of every note of `group` from `time`, over `timeConstant`. Notes\r\n * that would only sound after `time` are cancelled.\r\n */\r\n damp(group: string, time: number, timeConstant: number): void;\r\n}\r\n\r\n/** How a zone's sample is played: its level correction and where it starts. */\r\nexport interface ZoneTrim {\r\n /** Evens the zone's level out against the preset's other zones. */\r\n gain: number;\r\n /** Seconds into the sample it becomes audible. */\r\n start: number;\r\n}\r\n\r\n/** A note's fade-in, over the sample's own attack, so no start clicks. */\r\nconst ATTACK_SECONDS = 0.002;\r\n/** The last fade, before a sample without a loop runs out or `maxRing`. */\r\nconst END_FADE_SECONDS = 0.08;\r\n/** After this many time constants a damped note is silent (−60 dB). */\r\nconst TIME_CONSTANTS_TO_SILENCE = 7;\r\n/** How much a note's gain follows its force: `velocity ** 1.5`. */\r\nconst VELOCITY_CURVE = 1.5;\r\n/** A tone filter one octave up the keyboard opens this share of an octave. */\r\nconst TONE_KEY_TRACKING = 0.5;\r\nconst TONE_MAX_HZ = 18000;\r\n/** Resonance (dB) of a flat, Butterworth-like filter in the Web Audio API. */\r\nconst FLAT_Q_DB = -3;\r\n/** How far a zone's level may be evened out, either way. */\r\nconst MAX_ZONE_CORRECTION = 2;\r\n/** The stretch of each sample, after its peak, whose level is compared. */\r\nconst ZONE_LEVEL_SECONDS = 0.3;\r\n/** Where a sample's peak is looked for: its attack. */\r\nconst ZONE_PEAK_SECONDS = 0.5;\r\n/** A sample becomes audible where it first reaches this share of its peak (−40 dB). */\r\nconst AUDIBLE_SHARE = 0.01;\r\n/** How much of the silence before that a note keeps, so its onset stays whole. */\r\nconst ONSET_MARGIN_SECONDS = 0.0005;\r\n/** WebAudioFont's rate for a zone that names none. */\r\nconst DEFAULT_SAMPLE_RATE = 44100;\r\n/** The band a bass's drive adds its harmonics in (Hz). */\r\nconst DRIVE_BAND = { low: 120, high: 1600 } as const;\r\n/** How hard the drive saturates. */\r\nconst DRIVE_GAIN = 3;\r\n/** Its lean to one side, for the even harmonics an amplifier adds. */\r\nconst DRIVE_ASYMMETRY = 0.2;\r\n\r\nexport const ROOM = {\r\n /** Seconds the room takes to fall 60 dB. */\r\n decay: 1.3,\r\n /** Seconds before the first reflection. */\r\n preDelay: 0.012,\r\n /** How much darker the tail grows than the first reflections (0–1). */\r\n damping: 0.85,\r\n} as const;\r\n\r\nconst MASTER_GAIN = 0.7;\r\n/**\r\n * A peak limiter, not a compressor: it touches only the loudest strum attacks\r\n * (the top 4 dB), so a pick's transient keeps its bite. Its automatic makeup\r\n * gain lifts the whole mix about 2 dB.\r\n */\r\nconst LIMITER = {\r\n threshold: -4,\r\n knee: 3,\r\n ratio: 12,\r\n attack: 0.002,\r\n release: 0.12,\r\n} as const;\r\n\r\n/** A zone's key: its root, after the soundfont's tuning (semitones). */\r\nexport function getZoneRoot(zone: WebAudioFontZone): number {\r\n return (\r\n (zone.originalPitch ?? 6000) / 100 -\r\n (zone.coarseTune ?? 0) -\r\n (zone.fineTune ?? 0) / 100\r\n );\r\n}\r\n\r\n/**\r\n * The zone that sounds `pitch`: of those whose key range holds it, the one\r\n * whose root is nearest; else the zone whose range is nearest.\r\n */\r\nexport function findZone(\r\n preset: WebAudioFontPreset,\r\n pitch: number,\r\n): WebAudioFontZone | null {\r\n let best: WebAudioFontZone | null = null;\r\n let bestDistance = Infinity;\r\n\r\n for (const zone of preset.zones) {\r\n const low = zone.keyRangeLow ?? 0;\r\n const high = zone.keyRangeHigh ?? 127;\r\n const outside = pitch < low ? low - pitch : pitch > high ? pitch - high : 0;\r\n // A zone that holds the pitch always beats one that does not.\r\n const distance = outside * 1000 + Math.abs(pitch - getZoneRoot(zone));\r\n\r\n if (distance < bestDistance) {\r\n best = zone;\r\n bestDistance = distance;\r\n }\r\n }\r\n\r\n return best;\r\n}\r\n\r\n/** Whether the zone has a sustain loop (WebAudioFont's rule). */\r\nexport function hasLoop(zone: WebAudioFontZone): boolean {\r\n return (\r\n (zone.loopStart ?? 0) >= 1 && (zone.loopEnd ?? 0) > (zone.loopStart ?? 0)\r\n );\r\n}\r\n\r\n/** Low-pass cutoff (Hz) for a note of `velocity` at `pitch`. */\r\nexport function getToneCutoff(\r\n sound: StrumSound,\r\n velocity: number,\r\n pitch: number,\r\n): number {\r\n const force = Math.min(1, Math.max(0, (velocity - 0.4) / 0.6));\r\n const cutoff =\r\n sound.tone.soft *\r\n (sound.tone.hard / sound.tone.soft) ** force *\r\n 2 ** ((TONE_KEY_TRACKING * (pitch - 60)) / 12);\r\n\r\n return Math.min(TONE_MAX_HZ, Math.max(sound.tone.soft / 2, cutoff));\r\n}\r\n\r\n/**\r\n * The gain a note of `velocity` sounds at, before its zone and instrument:\r\n * `velocity ** 1.5`, as far as the instrument's `dynamics` lets force count.\r\n */\r\nexport function getVelocityGain(velocity: number, dynamics = 1): number {\r\n const force = Math.max(0, Math.min(1, velocity)) ** VELOCITY_CURVE;\r\n\r\n return 1 - dynamics + dynamics * force;\r\n}\r\n\r\n/**\r\n * The drive's transfer curve: a soft saturation leaning to one side, so it\r\n * adds both odd and even harmonics, from −1 to 1 over `size` points.\r\n */\r\nexport function createDriveCurve(size = 1024): Float32Array<ArrayBuffer> {\r\n const curve = new Float32Array(size);\r\n\r\n for (let i = 0; i < size; i += 1) {\r\n const x = (i / (size - 1)) * 2 - 1;\r\n\r\n curve[i] = Math.tanh(DRIVE_GAIN * x) + DRIVE_ASYMMETRY * x * x;\r\n }\r\n\r\n return curve;\r\n}\r\n\r\n/**\r\n * A sample's level — its RMS over {@link ZONE_LEVEL_SECONDS} from its peak —\r\n * and where it becomes audible, just before it first reaches −40 dB.\r\n */\r\nfunction measureSample(buffer: AudioBuffer): { level: number; start: number } {\r\n const data = buffer.getChannelData(0);\r\n const rate = buffer.sampleRate;\r\n const attackEnd = Math.min(data.length, Math.round(ZONE_PEAK_SECONDS * rate));\r\n let peak = 0;\r\n let peakIndex = 0;\r\n\r\n for (let i = 0; i < attackEnd; i += 1) {\r\n const value = Math.abs(data[i]);\r\n\r\n if (value > peak) {\r\n peak = value;\r\n peakIndex = i;\r\n }\r\n }\r\n\r\n let audible = 0;\r\n\r\n while (\r\n audible < peakIndex &&\r\n Math.abs(data[audible]) < peak * AUDIBLE_SHARE\r\n ) {\r\n audible += 1;\r\n }\r\n\r\n const end = Math.min(\r\n data.length,\r\n peakIndex + Math.round(ZONE_LEVEL_SECONDS * rate),\r\n );\r\n let sum = 0;\r\n\r\n for (let i = peakIndex; i < end; i += 1) {\r\n sum += data[i] * data[i];\r\n }\r\n\r\n return {\r\n level: Math.sqrt(sum / Math.max(1, end - peakIndex)),\r\n start: Math.max(0, audible / rate - ONSET_MARGIN_SECONDS),\r\n };\r\n}\r\n\r\nconst zoneTrims = new WeakMap<\r\n WebAudioFontPreset,\r\n Map<WebAudioFontZone, ZoneTrim>\r\n>();\r\n\r\n/**\r\n * How to play a zone's sample, measured once per preset: the gain that evens\r\n * its level out — the preset's median level over the zone's, within ×½ to\r\n * ×2 — and the silence to skip before it.\r\n */\r\nexport function getZoneTrim(\r\n preset: WebAudioFontPreset,\r\n zone: WebAudioFontZone,\r\n): ZoneTrim {\r\n let trims = zoneTrims.get(preset);\r\n\r\n if (!trims) {\r\n const measured = new Map<\r\n WebAudioFontZone,\r\n { level: number; start: number }\r\n >();\r\n\r\n for (const each of preset.zones) {\r\n if (each.buffer) {\r\n measured.set(each, measureSample(each.buffer));\r\n }\r\n }\r\n\r\n const levels = [...measured.values()]\r\n .map(({ level }) => level)\r\n .filter((level) => level > 0)\r\n .sort((a, b) => a - b);\r\n const median = levels[Math.floor(levels.length / 2)] ?? 0;\r\n\r\n trims = new Map();\r\n\r\n for (const [each, { level, start }] of measured) {\r\n trims.set(each, {\r\n gain:\r\n level > 0 && median > 0\r\n ? Math.min(\r\n MAX_ZONE_CORRECTION,\r\n Math.max(1 / MAX_ZONE_CORRECTION, median / level),\r\n )\r\n : 1,\r\n start,\r\n });\r\n }\r\n\r\n zoneTrims.set(preset, trims);\r\n }\r\n\r\n return trims.get(zone) ?? { gain: 1, start: 0 };\r\n}\r\n\r\n/**\r\n * A stereo room impulse: noise decaying `decay` seconds to −60 dB after a\r\n * short pre-delay, darkening as it fades, each channel drawn apart so the room\r\n * sounds wide.\r\n */\r\nexport function createRoomImpulse(\r\n sampleRate: number,\r\n random: () => number = Math.random,\r\n { decay, preDelay, damping }: typeof ROOM = ROOM,\r\n): [Float32Array, Float32Array] {\r\n const length = Math.round((preDelay + decay) * sampleRate);\r\n const delay = Math.round(preDelay * sampleRate);\r\n const channels: [Float32Array, Float32Array] = [\r\n new Float32Array(length),\r\n new Float32Array(length),\r\n ];\r\n\r\n for (const data of channels) {\r\n let low = 0;\r\n\r\n for (let i = delay; i < length; i += 1) {\r\n const t = (i - delay) / sampleRate;\r\n // −60 dB at `decay`: e^(−6.9·t/decay).\r\n const level = Math.exp((-6.9 * t) / decay);\r\n // A one-pole low-pass closing as the tail fades: air and walls take the\r\n // highs first.\r\n const smoothing = damping * Math.min(1, t / decay) ** 0.5;\r\n\r\n low = low * smoothing + (random() * 2 - 1) * (1 - smoothing);\r\n data[i] = low * level;\r\n }\r\n }\r\n\r\n return channels;\r\n}\r\n\r\ninterface Bus {\r\n input: GainNode;\r\n}\r\n\r\ninterface Voice extends StrumVoice {\r\n slot: string;\r\n group: string;\r\n /** When the note stops sounding (scheduled). */\r\n end: number;\r\n source: AudioBufferSourceNode;\r\n damper: GainNode;\r\n /** Settled already: by a damp, a cancel, or its end. */\r\n done: boolean;\r\n}\r\n\r\nexport class WebAudioStrumSoundEngine implements StrumSoundEngine {\r\n #context: BaseAudioContext;\r\n #master: GainNode;\r\n #room: AudioNode;\r\n #buses = new Map<StrumSound, Bus>();\r\n #slots = new Map<string, Voice>();\r\n #groups = new Map<string, Set<Voice>>();\r\n\r\n constructor(\r\n context: BaseAudioContext,\r\n destination: AudioNode = context.destination,\r\n ) {\r\n this.#context = context;\r\n\r\n const limiter = context.createDynamicsCompressor();\r\n\r\n limiter.threshold.value = LIMITER.threshold;\r\n limiter.knee.value = LIMITER.knee;\r\n limiter.ratio.value = LIMITER.ratio;\r\n limiter.attack.value = LIMITER.attack;\r\n limiter.release.value = LIMITER.release;\r\n limiter.connect(destination);\r\n\r\n this.#master = context.createGain();\r\n this.#master.gain.value = MASTER_GAIN;\r\n this.#master.connect(limiter);\r\n\r\n const convolver = context.createConvolver();\r\n const impulse = context.createBuffer(\r\n 2,\r\n Math.round((ROOM.preDelay + ROOM.decay) * context.sampleRate),\r\n context.sampleRate,\r\n );\r\n\r\n createRoomImpulse(context.sampleRate).forEach((data, channel) =>\r\n impulse.getChannelData(channel).set(data),\r\n );\r\n convolver.buffer = impulse;\r\n convolver.connect(this.#master);\r\n this.#room = convolver;\r\n }\r\n\r\n play(note: StrumNote): StrumVoice | null {\r\n const zone = findZone(note.preset, note.pitch);\r\n const buffer = zone?.buffer;\r\n\r\n if (!zone || !buffer) {\r\n return null;\r\n }\r\n\r\n const context = this.#context;\r\n const { sound } = note;\r\n const time = Math.max(note.time, context.currentTime);\r\n const rate =\r\n 2 ** ((note.pitch - getZoneRoot(zone) + (note.detune ?? 0) / 100) / 12);\r\n const source = context.createBufferSource();\r\n const loops = hasLoop(zone);\r\n const delay = zone.delay ?? 0;\r\n const trim = getZoneTrim(note.preset, zone);\r\n const offset = delay + trim.start;\r\n\r\n source.buffer = buffer;\r\n source.playbackRate.value = rate;\r\n\r\n if (loops) {\r\n const sampleRate = zone.sampleRate ?? DEFAULT_SAMPLE_RATE;\r\n\r\n source.loop = true;\r\n source.loopStart = zone.loopStart! / sampleRate + delay;\r\n source.loopEnd = zone.loopEnd! / sampleRate + delay;\r\n }\r\n\r\n // How long the sample plays, in real time, before its loop or its end.\r\n const natural =\r\n Math.max(0, (loops ? source.loopStart : buffer.duration) - offset) / rate;\r\n const ring = Math.max(\r\n ATTACK_SECONDS + END_FADE_SECONDS,\r\n loops ? sound.maxRing : Math.min(sound.maxRing, natural),\r\n );\r\n const end = time + ring;\r\n\r\n const tone = context.createBiquadFilter();\r\n\r\n tone.type = 'lowpass';\r\n tone.frequency.value = getToneCutoff(sound, note.velocity, note.pitch);\r\n tone.Q.value = FLAT_Q_DB;\r\n\r\n const envelope = context.createGain();\r\n const gain = envelope.gain;\r\n\r\n gain.setValueAtTime(0, time);\r\n gain.linearRampToValueAtTime(\r\n getVelocityGain(note.velocity, sound.dynamics) * trim.gain,\r\n time + ATTACK_SECONDS,\r\n );\r\n\r\n if (sound.sustain > 0) {\r\n gain.setTargetAtTime(0, time + ATTACK_SECONDS, sound.sustain);\r\n }\r\n\r\n if (loops && natural < ring) {\r\n gain.setTargetAtTime(\r\n 0,\r\n time + Math.max(ATTACK_SECONDS, natural),\r\n sound.tail,\r\n );\r\n }\r\n\r\n gain.setTargetAtTime(0, end - END_FADE_SECONDS, END_FADE_SECONDS / 5);\r\n\r\n const damper = context.createGain();\r\n const panner =\r\n note.pan && typeof context.createStereoPanner === 'function'\r\n ? context.createStereoPanner()\r\n : null;\r\n\r\n source.connect(tone);\r\n tone.connect(envelope);\r\n envelope.connect(damper);\r\n\r\n if (panner) {\r\n panner.pan.value = Math.max(-1, Math.min(1, note.pan!));\r\n damper.connect(panner);\r\n panner.connect(this.#getBus(sound).input);\r\n } else {\r\n damper.connect(this.#getBus(sound).input);\r\n }\r\n\r\n const voice: Voice = {\r\n time,\r\n end,\r\n slot: note.slot,\r\n group: note.group,\r\n source,\r\n damper,\r\n done: false,\r\n cancel: () => {\r\n if (!voice.done && voice.time > context.currentTime) {\r\n this.#stopVoice(voice, context.currentTime);\r\n }\r\n },\r\n };\r\n\r\n source.onended = () => {\r\n voice.done = true;\r\n this.#forget(voice);\r\n source.disconnect();\r\n damper.disconnect();\r\n panner?.disconnect();\r\n };\r\n source.start(time, offset);\r\n source.stop(end);\r\n\r\n const previous = this.#slots.get(note.slot);\r\n\r\n if (previous && !previous.done) {\r\n this.#dampVoice(previous, time, sound.damping);\r\n }\r\n\r\n this.#slots.set(note.slot, voice);\r\n\r\n let group = this.#groups.get(note.group);\r\n\r\n if (!group) {\r\n group = new Set();\r\n this.#groups.set(note.group, group);\r\n }\r\n\r\n group.add(voice);\r\n return voice;\r\n }\r\n\r\n damp(group: string, time: number, timeConstant: number): void {\r\n for (const voice of this.#groups.get(group) ?? []) {\r\n this.#dampVoice(voice, time, timeConstant);\r\n }\r\n }\r\n\r\n #dampVoice(voice: Voice, time: number, timeConstant: number) {\r\n if (voice.done) {\r\n return;\r\n }\r\n\r\n if (voice.time >= time) {\r\n this.#stopVoice(voice, this.#context.currentTime);\r\n return;\r\n }\r\n\r\n const stop = time + timeConstant * TIME_CONSTANTS_TO_SILENCE;\r\n\r\n voice.done = true;\r\n voice.damper.gain.setTargetAtTime(0, time, timeConstant);\r\n\r\n if (stop < voice.end) {\r\n voice.end = stop;\r\n stopSource(voice.source, stop);\r\n }\r\n }\r\n\r\n #stopVoice(voice: Voice, time: number) {\r\n voice.done = true;\r\n voice.end = time;\r\n stopSource(voice.source, time);\r\n this.#forget(voice);\r\n }\r\n\r\n #forget(voice: Voice) {\r\n if (this.#slots.get(voice.slot) === voice) {\r\n this.#slots.delete(voice.slot);\r\n }\r\n\r\n this.#groups.get(voice.group)?.delete(voice);\r\n }\r\n\r\n #getBus(sound: StrumSound): Bus {\r\n let bus = this.#buses.get(sound);\r\n\r\n if (!bus) {\r\n const context = this.#context;\r\n const input = context.createGain();\r\n const highpass = context.createBiquadFilter();\r\n const send = context.createGain();\r\n let output: AudioNode = highpass;\r\n\r\n input.gain.value = sound.level;\r\n highpass.type = 'highpass';\r\n highpass.frequency.value = sound.body.highpass;\r\n highpass.Q.value = FLAT_Q_DB;\r\n input.connect(highpass);\r\n\r\n if (sound.body.presence) {\r\n const presence = context.createBiquadFilter();\r\n\r\n presence.type = 'peaking';\r\n presence.frequency.value = sound.body.presence.frequency;\r\n presence.gain.value = sound.body.presence.gain;\r\n presence.Q.value = 1;\r\n highpass.connect(presence);\r\n output = presence;\r\n }\r\n\r\n if (sound.body.drive) {\r\n output = this.#addDrive(output, sound.body.drive);\r\n }\r\n\r\n send.gain.value = sound.room;\r\n output.connect(this.#master);\r\n output.connect(send);\r\n send.connect(this.#room);\r\n bus = { input };\r\n this.#buses.set(sound, bus);\r\n }\r\n\r\n return bus;\r\n }\r\n\r\n /**\r\n * Mixes a drive into `body`: the dry sound plus `amount` of its soft\r\n * saturation, kept to {@link DRIVE_BAND}. Returns the node carrying both.\r\n */\r\n #addDrive(body: AudioNode, amount: number): AudioNode {\r\n const context = this.#context;\r\n const sum = context.createGain();\r\n const shaper = context.createWaveShaper();\r\n const low = context.createBiquadFilter();\r\n const high = context.createBiquadFilter();\r\n const mix = context.createGain();\r\n\r\n shaper.curve = createDriveCurve();\r\n shaper.oversample = '2x';\r\n // The high-pass also takes away the curve's lean, a DC offset.\r\n low.type = 'highpass';\r\n low.frequency.value = DRIVE_BAND.low;\r\n low.Q.value = FLAT_Q_DB;\r\n high.type = 'lowpass';\r\n high.frequency.value = DRIVE_BAND.high;\r\n high.Q.value = FLAT_Q_DB;\r\n mix.gain.value = amount;\r\n body.connect(sum);\r\n body.connect(shaper);\r\n shaper.connect(low);\r\n low.connect(high);\r\n high.connect(mix);\r\n mix.connect(sum);\r\n return sum;\r\n }\r\n}\r\n\r\n/** `stop` again, to bring a scheduled end forward; old Safari throws on it. */\r\nfunction stopSource(source: AudioBufferSourceNode, time: number) {\r\n try {\r\n source.stop(time);\r\n } catch {\r\n // Already stopped: nothing left to silence.\r\n }\r\n}\r\n","import type { StrumSoundfont } from './instruments.ts';\r\nimport webAudioFontPlayerUrl from './assets/webaudiofont/WebAudioFontPlayer.js?url';\r\n\r\n/**\r\n * The strum player's soundfonts are loaded and decoded by Sergey Surikov's\r\n * WebAudioFont, at runtime, with a `<script>` per file from copies this app\r\n * serves itself (`assets/`, whose README says where each came from). No other\r\n * host is involved. The notes are then played by this repo's own voice engine\r\n * (`StrumSoundEngine.ts`), not by WebAudioFont's player:\r\n *\r\n * - The player is webaudiofont **2.5.57**, the npm package's\r\n * `npm/dist/WebAudioFontPlayer.js` byte for byte. It is GPL-3.0-or-later,\r\n * so it stays a file of its own, beside its license, rather than bundled\r\n * into this MIT code. Only its `adjustPreset` is called, to decode the\r\n * samples.\r\n * - The soundfonts are MIT files from `surikov.github.io/webaudiofontdata`\r\n * (see `STRUM_SOUNDFONTS`).\r\n *\r\n * Every file carries a `sha384-` pin, and it is checked **where a pin can be\r\n * trusted: at pack time**, by `scripts/pack-packages.ts`, against the asset the\r\n * build actually emitted. It is deliberately NOT sent as an `integrity`\r\n * attribute at runtime — see `loadScriptOnce`. Each preset is decoded\r\n * completely before it is handed back, so the first stroke of a fresh load is\r\n * never dropped.\r\n */\r\n\r\n/**\r\n * One sample of a soundfont and the keys it sounds. `adjustPreset` fills in\r\n * the numbers a data file leaves out, and decodes `buffer`.\r\n */\r\nexport interface WebAudioFontZone {\r\n buffer?: AudioBuffer;\r\n keyRangeLow?: number;\r\n keyRangeHigh?: number;\r\n /** The sample's pitch, in cents of a MIDI note (`6000` = middle C). */\r\n originalPitch?: number;\r\n /** Semitones and cents the zone retunes the sample by. */\r\n coarseTune?: number;\r\n fineTune?: number;\r\n /** The sample's own rate, which `loopStart` and `loopEnd` count in. */\r\n sampleRate?: number;\r\n loopStart?: number;\r\n loopEnd?: number;\r\n /** Seconds into the buffer the sample starts. */\r\n delay?: number;\r\n}\r\n\r\nexport interface WebAudioFontPreset {\r\n zones: WebAudioFontZone[];\r\n}\r\n\r\nexport interface WebAudioFontPlayerInstance {\r\n /** Decodes every zone's sample into its `buffer`, some asynchronously. */\r\n adjustPreset(context: BaseAudioContext, preset: WebAudioFontPreset): void;\r\n}\r\n\r\nexport type WebAudioFontPlayerConstructor =\r\n new () => WebAudioFontPlayerInstance;\r\n\r\n/**\r\n * `integrity` is the pin `pack-packages.ts` checks the emitted asset against.\r\n * Nothing reads it at runtime; it is data, and it must stay in the bundle for\r\n * that check to have something to check.\r\n */\r\nexport const WEBAUDIOFONT_PLAYER_SCRIPT = {\r\n url: webAudioFontPlayerUrl,\r\n integrity:\r\n 'sha384-Dvf7U4nlcDoz6ty0yygUFW1OkoynFUAy62/rMn8ERLrMEaW5Spe0Vqt2oq6gJw6U',\r\n} as const;\r\n\r\nconst DECODE_POLL_MS = 25;\r\nconst DECODE_TIMEOUT_MS = 30_000;\r\n\r\nconst scriptLoads = new Map<string, Promise<void>>();\r\n\r\n/**\r\n * Adds `<script src>` once per URL; a failed load may be retried.\r\n *\r\n * **No `integrity` / `crossOrigin` here, on purpose.** These URLs are not a\r\n * CDN's — they are whatever the host's bundler resolved `?url` to, served from\r\n * the host's own origin, out of files shipped inside this package. SRI guards\r\n * against a third party serving different bytes; there is no third party on\r\n * this path, and the attribute cannot survive one either:\r\n *\r\n * - a Vite dev host serves a `.js` file under `node_modules` through its\r\n * transform pipeline with an inline sourcemap appended (the player script:\r\n * 108,616 bytes on disk, 420,383 served), so the hash never matches and the\r\n * chord simply does not sound;\r\n * - `crossOrigin=\"anonymous\"` makes it a CORS request, which a custom\r\n * app protocol (`owa://…`) need not answer.\r\n *\r\n * The guarantee the pin is there for — a copy that drifted by a byte must not\r\n * ship — is kept, and kept *earlier*: `scripts/pack-packages.ts` hashes the\r\n * asset the build emitted and fails the pack on a mismatch, rather than\r\n * leaving it to fail on a projector in a church hall.\r\n */\r\nexport function loadScriptOnce(url: string): Promise<void> {\r\n const pending = scriptLoads.get(url);\r\n\r\n if (pending) {\r\n return pending;\r\n }\r\n\r\n const load = new Promise<void>((resolve, reject) => {\r\n const script = document.createElement('script');\r\n\r\n script.src = url;\r\n script.async = true;\r\n script.addEventListener('load', () => resolve(), { once: true });\r\n script.addEventListener(\r\n 'error',\r\n () => {\r\n scriptLoads.delete(url);\r\n script.remove();\r\n reject(new Error(`Could not load ${url} (not served, or blocked).`));\r\n },\r\n { once: true },\r\n );\r\n document.head.appendChild(script);\r\n });\r\n\r\n scriptLoads.set(url, load);\r\n return load;\r\n}\r\n\r\nexport async function loadWebAudioFontPlayer(): Promise<WebAudioFontPlayerConstructor> {\r\n await loadScriptOnce(WEBAUDIOFONT_PLAYER_SCRIPT.url);\r\n\r\n const Player = (globalThis as any).WebAudioFontPlayer;\r\n\r\n if (typeof Player !== 'function') {\r\n throw new Error('The WebAudioFont script loaded but defined no player.');\r\n }\r\n\r\n return Player;\r\n}\r\n\r\nfunction waitForDecodedZones(preset: WebAudioFontPreset): Promise<void> {\r\n const startedAt = Date.now();\r\n\r\n return new Promise((resolve, reject) => {\r\n const check = () => {\r\n if (preset.zones.every((zone) => zone.buffer)) {\r\n resolve();\r\n } else if (Date.now() - startedAt > DECODE_TIMEOUT_MS) {\r\n reject(new Error('Timed out decoding the soundfont samples.'));\r\n } else {\r\n setTimeout(check, DECODE_POLL_MS);\r\n }\r\n };\r\n\r\n check();\r\n });\r\n}\r\n\r\n/**\r\n * Loads a soundfont's data file and decodes every sample of it\r\n * (`player.adjustPreset`), then waits until each zone has its buffer.\r\n */\r\nexport async function loadSoundfontPreset(\r\n player: WebAudioFontPlayerInstance,\r\n context: BaseAudioContext,\r\n soundfont: StrumSoundfont,\r\n): Promise<WebAudioFontPreset> {\r\n await loadScriptOnce(soundfont.url);\r\n\r\n const preset = (globalThis as any)[soundfont.variable];\r\n\r\n if (!preset || !Array.isArray(preset.zones)) {\r\n throw new Error(`${soundfont.key} loaded but defined no preset.`);\r\n }\r\n\r\n player.adjustPreset(context, preset);\r\n await waitForDecodedZones(preset);\r\n return preset;\r\n}\r\n","export default \"__VITE_ASSET__6kssa3xCT2YlMpIbbRNP$Q__\"","import { getStrumSound, type StrumSound } from './instrument-sounds.ts';\r\nimport {\r\n getStrumInstrument,\r\n type StrumInstrument,\r\n type StrumInstrumentId,\r\n type StrumSoundfont,\r\n} from './instruments.ts';\r\nimport { getStrumStepSeconds, type StrumPattern } from './patterns.ts';\r\nimport {\r\n planChordRoll,\r\n planStroke,\r\n type PlannedNote,\r\n type StrokePlan,\r\n} from './strum-plan.ts';\r\nimport {\r\n WebAudioStrumSoundEngine,\r\n type StrumSoundEngine,\r\n type StrumVoice,\r\n} from './StrumSoundEngine.ts';\r\nimport {\r\n loadSoundfontPreset,\r\n loadWebAudioFontPlayer,\r\n type WebAudioFontPlayerConstructor,\r\n type WebAudioFontPlayerInstance,\r\n type WebAudioFontPreset,\r\n} from './webaudiofont.ts';\r\n\r\n/**\r\n * Plays one strum pattern at a time, looping, on the audio clock: step 1\r\n * sounds {@link START_DELAY_SECONDS} after the press, each later step one\r\n * step-length after the one before (`getStrumStepSeconds`), and after the last\r\n * step the grid starts again. A short timer queues strokes up to\r\n * {@link SCHEDULE_AHEAD_SECONDS} ahead. Each stroke is played the way its\r\n * instrument is (`planStroke`) and sounded by the voice engine\r\n * (`StrumSoundEngine.ts`), where every string rings until it is struck again.\r\n *\r\n * A stop cancels the queued strokes that have not sounded yet, lets a stroke\r\n * under way finish, and then damps the strings still ringing, as a player's\r\n * palm does (the instrument's `release`).\r\n *\r\n * It also plays a chord card's roll (`planChordRoll`), alongside a pattern and\r\n * untouched by a stop. A new roll damps the one before it.\r\n */\r\n\r\nexport interface StrumPlaybackRequest {\r\n pattern: StrumPattern;\r\n tempo: number;\r\n timeSignature: string;\r\n /** MIDI notes the pattern strums, in string order (piano: low to high). */\r\n chord: readonly number[];\r\n instrument: StrumInstrumentId;\r\n}\r\n\r\nexport interface StrumChordRequest {\r\n /** The chord's notes, in the order they are struck. */\r\n midi: readonly number[];\r\n instrument: StrumInstrumentId;\r\n}\r\n\r\nexport type StrumPlayerState = 'idle' | 'loading' | 'playing';\r\n\r\n/** Browser seams, swapped out by the unit tests. */\r\nexport interface StrumPlayerRuntime {\r\n createAudioContext(): AudioContext;\r\n loadPlayer(): Promise<WebAudioFontPlayerConstructor>;\r\n loadPreset(\r\n player: WebAudioFontPlayerInstance,\r\n context: BaseAudioContext,\r\n soundfont: StrumSoundfont,\r\n ): Promise<WebAudioFontPreset>;\r\n /** The voice engine that sounds the notes, on the player's context. */\r\n createEngine(context: AudioContext): StrumSoundEngine;\r\n /** Draws a player's human wander, `0 ≤ n < 1`. Default `Math.random`. */\r\n random?(): number;\r\n}\r\n\r\nexport const browserStrumPlayerRuntime: StrumPlayerRuntime = {\r\n createAudioContext() {\r\n const AudioContextConstructor =\r\n globalThis.AudioContext ?? (globalThis as any).webkitAudioContext;\r\n\r\n return new AudioContextConstructor();\r\n },\r\n loadPlayer: loadWebAudioFontPlayer,\r\n loadPreset: loadSoundfontPreset,\r\n createEngine: (context) => new WebAudioStrumSoundEngine(context),\r\n};\r\n\r\nconst SCHEDULER_INTERVAL_MS = 25;\r\nexport const SCHEDULE_AHEAD_SECONDS = 0.1;\r\n/** Head room between the press and the first stroke. */\r\nexport const START_DELAY_SECONDS = 0.05;\r\n/** Two times this close are the same instant. */\r\nconst TIME_EPSILON = 1e-6;\r\n/** The engine groups a pattern's notes, and a chord card's, apart. */\r\nexport const PATTERN_GROUP = 'pattern';\r\nexport const CHORD_GROUP = 'chord';\r\n\r\ninterface Playback {\r\n context: AudioContext;\r\n engine: StrumSoundEngine;\r\n preset: WebAudioFontPreset;\r\n instrument: StrumInstrumentId;\r\n sound: StrumSound;\r\n pattern: StrumPattern;\r\n chord: readonly number[];\r\n stepSeconds: number;\r\n startTime: number;\r\n /** How many step-lengths after the start the next step sounds. */\r\n nextTick: number;\r\n /** The 0-based step of the pattern that tick plays. */\r\n nextStep: number;\r\n}\r\n\r\n/** The notes one stroke scheduled — cancelled if a stop beats it. */\r\ninterface StrokeRecord {\r\n /** When its first note sounds. */\r\n time: number;\r\n /** When its last note sounds. */\r\n end: number;\r\n voices: StrumVoice[];\r\n}\r\n\r\nexport class StrumPlayer {\r\n onStep: ((patternId: string, stepNumber: number) => void) | null = null;\r\n onStateChange:\r\n | ((\r\n state: StrumPlayerState,\r\n patternId: string | null,\r\n error?: unknown,\r\n ) => void)\r\n | null = null;\r\n\r\n #runtime: StrumPlayerRuntime;\r\n #context: AudioContext | null = null;\r\n #engine: StrumSoundEngine | null = null;\r\n #playerLoad: Promise<WebAudioFontPlayerInstance> | null = null;\r\n #presetLoads = new Map<string, Promise<WebAudioFontPreset>>();\r\n #playback: Playback | null = null;\r\n #state: StrumPlayerState = 'idle';\r\n #statePatternId: string | null = null;\r\n #token = 0;\r\n #chordToken = 0;\r\n #records: StrokeRecord[] = [];\r\n #timer: ReturnType<typeof setInterval> | null = null;\r\n #uiTimers = new Set<ReturnType<typeof setTimeout>>();\r\n\r\n constructor(runtime: StrumPlayerRuntime = browserStrumPlayerRuntime) {\r\n this.#runtime = runtime;\r\n }\r\n\r\n get state(): StrumPlayerState {\r\n return this.#state;\r\n }\r\n\r\n /** The pattern loading or playing, if any. */\r\n get patternId(): string | null {\r\n return this.#statePatternId;\r\n }\r\n\r\n /**\r\n * Creates and resumes the audio context without playing anything. Browsers\r\n * only allow that from a user gesture, before its first `await` — so a\r\n * caller with its own async work to do before {@link play} calls this first.\r\n */\r\n unlock(): void {\r\n void this.#ensureContext().resume?.();\r\n }\r\n\r\n /**\r\n * Loads and decodes a soundfont ahead of a play. Creates the audio context,\r\n * so call it from a user gesture.\r\n */\r\n async preload(soundfont: StrumSoundfont): Promise<void> {\r\n const context = this.#ensureContext();\r\n const player = await this.#ensurePlayer();\r\n\r\n await this.#ensurePreset(player, context, soundfont);\r\n }\r\n\r\n /**\r\n * Stops whatever plays and starts `request`. Call it straight from the\r\n * user's gesture: the audio context is created and resumed before the first\r\n * `await`, as browsers require. Failures are reported through\r\n * `onStateChange`, not thrown.\r\n */\r\n async play(request: StrumPlaybackRequest): Promise<void> {\r\n this.stop();\r\n\r\n const token = ++this.#token;\r\n const context = this.#ensureContext();\r\n\r\n void context.resume?.();\r\n this.#setState('loading', request.pattern.id);\r\n\r\n try {\r\n const { soundfont } = getInstrument(request.instrument);\r\n const player = await this.#ensurePlayer();\r\n const preset = await this.#ensurePreset(player, context, soundfont);\r\n\r\n if (token === this.#token) {\r\n this.#start(request, context, preset);\r\n }\r\n } catch (error) {\r\n if (token === this.#token) {\r\n this.#setState('idle', null, error);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Plays a chord card's roll (`planChordRoll`). It sounds alongside a playing\r\n * pattern, and a stop leaves it ringing; the next roll damps it. Call it\r\n * from the user's gesture, like {@link play}.\r\n *\r\n * Of presses made while the soundfont loads, only the last one plays.\r\n * Resolves `true` once the notes are queued, `false` when a later press took\r\n * over; rejects when the player or soundfont fails to load.\r\n */\r\n async playChord(request: StrumChordRequest): Promise<boolean> {\r\n const token = ++this.#chordToken;\r\n const context = this.#ensureContext();\r\n\r\n void context.resume?.();\r\n\r\n const { soundfont } = getInstrument(request.instrument);\r\n const player = await this.#ensurePlayer();\r\n const preset = await this.#ensurePreset(player, context, soundfont);\r\n\r\n if (token !== this.#chordToken) {\r\n return false;\r\n }\r\n\r\n const engine = this.#ensureEngine(context);\r\n const sound = getStrumSound(request.instrument);\r\n const start = context.currentTime + START_DELAY_SECONDS;\r\n\r\n // The hand leaves the last roll's strings as it starts this one.\r\n engine.damp(CHORD_GROUP, start, sound.release);\r\n\r\n for (const note of planChordRoll(request.midi, sound, this.#random)) {\r\n this.#sound(engine, preset, sound, note, start, {\r\n slot: `${CHORD_GROUP}:${request.instrument}:${note.slot}`,\r\n group: CHORD_GROUP,\r\n });\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /** Swaps in an edited pattern mid-run; its next step plays the edit. */\r\n updatePattern(pattern: StrumPattern) {\r\n const playback = this.#playback;\r\n\r\n if (!playback || playback.pattern.id !== pattern.id) {\r\n return;\r\n }\r\n\r\n playback.pattern = pattern;\r\n\r\n if (playback.nextStep >= pattern.steps.length) {\r\n playback.nextStep = 0;\r\n }\r\n }\r\n\r\n stop() {\r\n this.#token += 1;\r\n\r\n const wasActive = this.#state !== 'idle';\r\n const playback = this.#playback;\r\n const now = this.#context?.currentTime ?? 0;\r\n let settle = now;\r\n\r\n this.#playback = null;\r\n this.#clearUiTimers();\r\n this.#stopTimer();\r\n\r\n for (const record of this.#records) {\r\n if (record.time > now + TIME_EPSILON) {\r\n record.voices.forEach((voice) => voice.cancel());\r\n } else {\r\n settle = Math.max(settle, record.end);\r\n }\r\n }\r\n\r\n this.#records = [];\r\n\r\n if (playback) {\r\n playback.engine.damp(PATTERN_GROUP, settle, playback.sound.release);\r\n }\r\n\r\n if (wasActive) {\r\n this.#setState('idle', null);\r\n }\r\n }\r\n\r\n #random = () => (this.#runtime.random ?? Math.random)();\r\n\r\n #ensureContext(): AudioContext {\r\n this.#context ??= this.#runtime.createAudioContext();\r\n return this.#context;\r\n }\r\n\r\n #ensureEngine(context: AudioContext): StrumSoundEngine {\r\n this.#engine ??= this.#runtime.createEngine(context);\r\n return this.#engine;\r\n }\r\n\r\n #ensurePlayer(): Promise<WebAudioFontPlayerInstance> {\r\n if (!this.#playerLoad) {\r\n const load = this.#runtime.loadPlayer().then((Player) => new Player());\r\n\r\n this.#playerLoad = load;\r\n load.catch(() => {\r\n if (this.#playerLoad === load) {\r\n this.#playerLoad = null;\r\n }\r\n });\r\n }\r\n\r\n return this.#playerLoad;\r\n }\r\n\r\n #ensurePreset(\r\n player: WebAudioFontPlayerInstance,\r\n context: BaseAudioContext,\r\n soundfont: StrumSoundfont,\r\n ): Promise<WebAudioFontPreset> {\r\n let load = this.#presetLoads.get(soundfont.key);\r\n\r\n if (!load) {\r\n const pending = this.#runtime.loadPreset(player, context, soundfont);\r\n\r\n load = pending;\r\n this.#presetLoads.set(soundfont.key, pending);\r\n pending.catch(() => {\r\n if (this.#presetLoads.get(soundfont.key) === pending) {\r\n this.#presetLoads.delete(soundfont.key);\r\n }\r\n });\r\n }\r\n\r\n return load;\r\n }\r\n\r\n #start(\r\n request: StrumPlaybackRequest,\r\n context: AudioContext,\r\n preset: WebAudioFontPreset,\r\n ) {\r\n this.#playback = {\r\n context,\r\n engine: this.#ensureEngine(context),\r\n preset,\r\n instrument: request.instrument,\r\n sound: getStrumSound(request.instrument),\r\n pattern: request.pattern,\r\n chord: [...request.chord],\r\n stepSeconds: getStrumStepSeconds(request.tempo, request.timeSignature),\r\n startTime: context.currentTime + START_DELAY_SECONDS,\r\n nextTick: 0,\r\n nextStep: 0,\r\n };\r\n\r\n this.#setState('playing', request.pattern.id);\r\n this.#timer ??= setInterval(() => this.#tick(), SCHEDULER_INTERVAL_MS);\r\n this.#tick();\r\n }\r\n\r\n #tick() {\r\n const playback = this.#playback;\r\n\r\n if (!playback || !playback.pattern.steps.length) {\r\n return;\r\n }\r\n\r\n const now = playback.context.currentTime;\r\n const horizon = now + SCHEDULE_AHEAD_SECONDS;\r\n let time = playback.startTime + playback.nextTick * playback.stepSeconds;\r\n\r\n while (time < horizon) {\r\n this.#playStep(playback, playback.nextStep, time);\r\n playback.nextTick += 1;\r\n playback.nextStep =\r\n (playback.nextStep + 1) % playback.pattern.steps.length;\r\n time = playback.startTime + playback.nextTick * playback.stepSeconds;\r\n }\r\n\r\n this.#records = this.#records.filter((record) => record.end > now);\r\n }\r\n\r\n #playStep(playback: Playback, index: number, time: number) {\r\n this.#showStep(playback, index + 1, time);\r\n\r\n const plan = planStroke(\r\n playback.chord,\r\n playback.pattern.steps[index],\r\n playback.sound,\r\n this.#random,\r\n );\r\n\r\n if (plan) {\r\n this.#perform(playback, plan, time);\r\n }\r\n }\r\n\r\n #perform(playback: Playback, plan: StrokePlan, time: number) {\r\n const onset = time + plan.shift;\r\n const record: StrokeRecord = { time: onset, end: onset, voices: [] };\r\n\r\n for (const note of plan.notes) {\r\n const voice = this.#sound(\r\n playback.engine,\r\n playback.preset,\r\n playback.sound,\r\n note,\r\n onset,\r\n {\r\n slot: `${PATTERN_GROUP}:${playback.instrument}:${note.slot}`,\r\n group: PATTERN_GROUP,\r\n },\r\n );\r\n\r\n record.end = Math.max(record.end, onset + note.offset);\r\n\r\n if (voice) {\r\n record.voices.push(voice);\r\n }\r\n }\r\n\r\n this.#records.push(record);\r\n }\r\n\r\n #sound(\r\n engine: StrumSoundEngine,\r\n preset: WebAudioFontPreset,\r\n sound: StrumSound,\r\n note: PlannedNote,\r\n start: number,\r\n { slot, group }: { slot: string; group: string },\r\n ): StrumVoice | null {\r\n return engine.play({\r\n preset,\r\n sound,\r\n pitch: note.pitch,\r\n time: start + note.offset,\r\n velocity: note.velocity,\r\n detune: note.detune,\r\n pan: note.pan,\r\n slot,\r\n group,\r\n });\r\n }\r\n\r\n #showStep(playback: Playback, stepNumber: number, time: number) {\r\n const delay = Math.max(0, (time - playback.context.currentTime) * 1000);\r\n const timer = setTimeout(() => {\r\n this.#uiTimers.delete(timer);\r\n\r\n if (playback === this.#playback) {\r\n this.onStep?.(playback.pattern.id, stepNumber);\r\n }\r\n }, delay);\r\n\r\n this.#uiTimers.add(timer);\r\n }\r\n\r\n #clearUiTimers() {\r\n this.#uiTimers.forEach((timer) => clearTimeout(timer));\r\n this.#uiTimers.clear();\r\n }\r\n\r\n #stopTimer() {\r\n if (this.#timer != null) {\r\n clearInterval(this.#timer);\r\n this.#timer = null;\r\n }\r\n }\r\n\r\n #setState(\r\n state: StrumPlayerState,\r\n patternId: string | null,\r\n error?: unknown,\r\n ) {\r\n this.#state = state;\r\n this.#statePatternId = patternId;\r\n this.onStateChange?.(state, patternId, error);\r\n }\r\n}\r\n\r\nfunction getInstrument(id: StrumInstrumentId): StrumInstrument {\r\n const instrument = getStrumInstrument(id);\r\n\r\n if (!instrument) {\r\n throw new Error(`Unknown instrument “${id}”.`);\r\n }\r\n\r\n return instrument;\r\n}\r\n","import {\r\n getChordCardMidi,\r\n loadChordDatabases,\r\n resolveChordCard,\r\n type ChordCard,\r\n type ChordDatabases,\r\n} from './chord-library.ts';\r\nimport {\r\n STRUM_INSTRUMENTS,\r\n getStrumInstrument,\r\n resolveStrumVoicing,\r\n type StrumInstrument,\r\n type StrumInstrumentId,\r\n type StrumVoicing,\r\n} from './instruments.ts';\r\nimport {\r\n parseOpenLyricPattern,\r\n type StrumPattern,\r\n type StrumPatternDefinition,\r\n} from './patterns.ts';\r\nimport {\r\n StrumPlayer,\r\n browserStrumPlayerRuntime,\r\n type StrumPlayerRuntime,\r\n type StrumPlayerState,\r\n} from './StrumPlayer.ts';\r\n\r\n/**\r\n * `OpenLyricPlayer` — the headless player: an Open Lyric song's strumming\r\n * patterns, and any chord Open Lyric can read, played on ten instruments —\r\n * acoustic, classical and electric guitar, piano, electric piano, organ,\r\n * ukulele, baritone ukulele, bass and upright bass. No DOM of its own;\r\n * `OpenLyricPlayerPlayground` is the UI built on it.\r\n *\r\n * It owns one {@link StrumPlayer} (one audio context, one soundfont cache) and\r\n * the chords-db tables the fretted voicings come from, loaded once on first\r\n * use. Every `play*` method must be called from a user gesture: the audio\r\n * context is unlocked before anything is awaited, as browsers require.\r\n */\r\n\r\nexport type OpenLyricPlayerState = StrumPlayerState;\r\n\r\nexport interface OpenLyricPlayerOptions {\r\n /** Browser seams (audio context, player + soundfont loaders); for tests. */\r\n runtime?: StrumPlayerRuntime;\r\n}\r\n\r\nexport interface OpenLyricPlayerStateEvent {\r\n state: OpenLyricPlayerState;\r\n /** The pattern loading or playing; `null` when idle. */\r\n patternId: string | null;\r\n /** Set when a play failed (the player script or soundfont did not load). */\r\n error?: unknown;\r\n}\r\n\r\nexport interface OpenLyricPlayerStepEvent {\r\n patternId: string;\r\n /** 1-based step of the pattern now sounding. */\r\n stepNumber: number;\r\n}\r\n\r\nexport interface OpenLyricPlayerEventMap {\r\n state: OpenLyricPlayerStateEvent;\r\n step: OpenLyricPlayerStepEvent;\r\n}\r\n\r\nexport interface OpenLyricPatternRequest {\r\n /**\r\n * The pattern: Open Lyric notation (`'d--u-- | d-u-d-'`), a\r\n * `{ id, label, source }` definition, or an already parsed pattern.\r\n */\r\n pattern: string | StrumPatternDefinition | StrumPattern;\r\n /** The song's key (`Config` → `Key`); a pattern strums its tonic chord. */\r\n key: string;\r\n /** Beats per minute, counting the time signature's unit. */\r\n tempo: number;\r\n /** Time signature (`Config` → `Time`). Default `4/4`. */\r\n time?: string;\r\n /** Default `guitar`. */\r\n instrument?: StrumInstrumentId;\r\n}\r\n\r\nconst DEFAULT_TIME = '4/4';\r\nconst DEFAULT_INSTRUMENT: StrumInstrumentId = 'guitar';\r\n\r\nfunction isParsedPattern(\r\n pattern: OpenLyricPatternRequest['pattern'],\r\n): pattern is StrumPattern {\r\n return typeof pattern === 'object' && Array.isArray(pattern.steps);\r\n}\r\n\r\nexport class OpenLyricPlayer {\r\n /** Every instrument, in display order. */\r\n static readonly instruments: readonly StrumInstrument[] = STRUM_INSTRUMENTS;\r\n\r\n #strum: StrumPlayer;\r\n #databases: ChordDatabases | null = null;\r\n #databaseLoad: Promise<ChordDatabases> | null = null;\r\n #listeners: {\r\n [K in keyof OpenLyricPlayerEventMap]: Set<\r\n (event: OpenLyricPlayerEventMap[K]) => void\r\n >;\r\n } = { state: new Set(), step: new Set() };\r\n\r\n constructor(options: OpenLyricPlayerOptions = {}) {\r\n this.#strum = new StrumPlayer(options.runtime ?? browserStrumPlayerRuntime);\r\n this.#strum.onStateChange = (state, patternId, error) =>\r\n this.#emit('state', { state, patternId, error });\r\n this.#strum.onStep = (patternId, stepNumber) =>\r\n this.#emit('step', { patternId, stepNumber });\r\n }\r\n\r\n get state(): OpenLyricPlayerState {\r\n return this.#strum.state;\r\n }\r\n\r\n /** The pattern loading or playing, if any. */\r\n get patternId(): string | null {\r\n return this.#strum.patternId;\r\n }\r\n\r\n /** Whether the chord tables have loaded — until then voicings are `null`. */\r\n get chordsLoaded(): boolean {\r\n return this.#databases !== null;\r\n }\r\n\r\n /** Subscribes to `state` or `step`; returns the unsubscribe. */\r\n on<K extends keyof OpenLyricPlayerEventMap>(\r\n type: K,\r\n listener: (event: OpenLyricPlayerEventMap[K]) => void,\r\n ): () => void {\r\n this.#listeners[type].add(listener);\r\n return () => {\r\n this.#listeners[type].delete(listener);\r\n };\r\n }\r\n\r\n /** Loads the chords-db tables once; every later call shares the load. */\r\n loadChords(): Promise<ChordDatabases> {\r\n if (!this.#databaseLoad) {\r\n const load = loadChordDatabases().then((databases) => {\r\n this.#databases = databases;\r\n return databases;\r\n });\r\n\r\n this.#databaseLoad = load;\r\n load.catch(() => {\r\n if (this.#databaseLoad === load) {\r\n this.#databaseLoad = null;\r\n }\r\n });\r\n }\r\n\r\n return this.#databaseLoad;\r\n }\r\n\r\n /**\r\n * The chord a pattern strums in `key` on `instrument` — the key's tonic\r\n * chord in that instrument's voicing. `null` when the key is unreadable,\r\n * the instrument has no voicing for it, or the chords have not loaded\r\n * ({@link loadChords}).\r\n */\r\n getVoicing(instrument: StrumInstrumentId, key: string): StrumVoicing | null {\r\n return this.#databases\r\n ? resolveStrumVoicing(instrument, key, this.#databases)\r\n : null;\r\n }\r\n\r\n /**\r\n * What `chord` looks like, and plays, on `instrument`: a chord diagram, piano\r\n * keys, or `none` with the reason. `null` until the chords have loaded.\r\n */\r\n resolveChord(instrument: StrumInstrumentId, chord: string): ChordCard | null {\r\n return this.#databases\r\n ? resolveChordCard(instrument, chord, this.#databases)\r\n : null;\r\n }\r\n\r\n /** Parses a pattern in Open Lyric notation against a time signature. */\r\n parsePattern(\r\n pattern: string | StrumPatternDefinition,\r\n time: string = DEFAULT_TIME,\r\n ): StrumPattern {\r\n const definition =\r\n typeof pattern === 'string'\r\n ? { id: 'pattern', label: 'Pattern', source: pattern }\r\n : pattern;\r\n\r\n return parseOpenLyricPattern(definition, time);\r\n }\r\n\r\n /**\r\n * Loads and decodes an instrument's soundfont ahead of a play. Creates the\r\n * audio context, so call it from a user gesture.\r\n */\r\n preload(instrument: StrumInstrumentId = DEFAULT_INSTRUMENT): Promise<void> {\r\n return this.#strum.preload(this.#getInstrument(instrument).soundfont);\r\n }\r\n\r\n /**\r\n * Stops whatever plays and loops `request.pattern` until {@link stop}.\r\n * Rejects when the pattern does not parse or there is nothing to strum (an\r\n * unreadable key, no voicing); a soundfont that fails to load is reported\r\n * through the `state` event instead, as `idle` with an `error`.\r\n */\r\n async playPattern(request: OpenLyricPatternRequest): Promise<void> {\r\n const time = request.time ?? DEFAULT_TIME;\r\n const instrument = this.#getInstrument(\r\n request.instrument ?? DEFAULT_INSTRUMENT,\r\n );\r\n const pattern = isParsedPattern(request.pattern)\r\n ? request.pattern\r\n : this.parsePattern(request.pattern, time);\r\n\r\n this.#strum.unlock();\r\n await this.loadChords();\r\n\r\n const voicing = this.getVoicing(instrument.id, request.key);\r\n\r\n if (!voicing) {\r\n throw new Error(\r\n `No ${instrument.label} voicing for the key of ${request.key}.`,\r\n );\r\n }\r\n\r\n await this.#strum.play({\r\n pattern,\r\n tempo: request.tempo,\r\n timeSignature: time,\r\n chord: voicing.midi,\r\n instrument: instrument.id,\r\n });\r\n }\r\n\r\n /**\r\n * Rolls `chord` once on `instrument`, alongside any playing pattern. Of\r\n * presses made while the soundfont loads only the last one plays: resolves\r\n * `true` once queued, `false` when a later press took over. Rejects when the\r\n * chord has nothing to play or the soundfont fails to load.\r\n */\r\n async playChord(\r\n chord: string,\r\n instrument: StrumInstrumentId = DEFAULT_INSTRUMENT,\r\n ): Promise<boolean> {\r\n const { label } = this.#getInstrument(instrument);\r\n\r\n this.#strum.unlock();\r\n\r\n const card = resolveChordCard(instrument, chord, await this.loadChords());\r\n const midi = getChordCardMidi(card);\r\n\r\n if (!midi.length) {\r\n throw new Error(\r\n card.kind === 'none' && card.reason === 'not-a-chord'\r\n ? `“${chord}” is not a chord Open Lyric can read.`\r\n : `No ${label} voicing for ${chord}.`,\r\n );\r\n }\r\n\r\n return this.#strum.playChord({ midi, instrument });\r\n }\r\n\r\n /** Swaps in an edited pattern mid-run; its next step plays the edit. */\r\n updatePattern(pattern: StrumPattern): void {\r\n this.#strum.updatePattern(pattern);\r\n }\r\n\r\n /**\r\n * Stops the pattern: a stroke under way finishes, then its ringing strings\r\n * are damped as a player's palm would. A chord roll rings on.\r\n */\r\n stop(): void {\r\n this.#strum.stop();\r\n }\r\n\r\n /** Drops every listener, then stops — silently. */\r\n destroy(): void {\r\n this.#listeners.state.clear();\r\n this.#listeners.step.clear();\r\n this.stop();\r\n }\r\n\r\n #getInstrument(id: StrumInstrumentId): StrumInstrument {\r\n const instrument = getStrumInstrument(id);\r\n\r\n if (!instrument) {\r\n throw new Error(`Unknown instrument “${id}”.`);\r\n }\r\n\r\n return instrument;\r\n }\r\n\r\n #emit<K extends keyof OpenLyricPlayerEventMap>(\r\n type: K,\r\n event: OpenLyricPlayerEventMap[K],\r\n ): void {\r\n for (const listener of this.#listeners[type]) {\r\n listener(event);\r\n }\r\n }\r\n}\r\n","import type { PianoVoicing } from './chord-library.ts';\r\nimport { getMidiNoteName } from './instruments.ts';\r\n\r\n/**\r\n * The piano figure a chord card draws — shared by the playground's chord card\r\n * and the chord popup's instrument view (`OpenLyricPluginPlayer`). Styled by\r\n * the `.piano-board*` classes in each surface's sheet.\r\n */\r\n\r\nconst WHITE_KEY_WIDTH = 18;\r\nconst WHITE_KEY_HEIGHT = 66;\r\nconst BLACK_KEY_WIDTH = 12;\r\nconst BLACK_KEY_HEIGHT = 40;\r\nconst BLACK_CHROMAS = new Set([1, 3, 6, 8, 10]);\r\nconst SVG_NS = 'http://www.w3.org/2000/svg';\r\n\r\nfunction createSvgElement<K extends keyof SVGElementTagNameMap>(\r\n name: K,\r\n attributes: Record<string, string | number>,\r\n): SVGElementTagNameMap[K] {\r\n const element = document.createElementNS(SVG_NS, name);\r\n\r\n for (const [attribute, value] of Object.entries(attributes)) {\r\n element.setAttribute(attribute, String(value));\r\n }\r\n\r\n return element;\r\n}\r\n\r\n/** A keyboard over the chord's octaves, its keys lit and named. */\r\nexport function createPianoBoard(\r\n chord: string,\r\n piano: PianoVoicing,\r\n keyText: string,\r\n) {\r\n const keys = Array.from(\r\n { length: piano.highest - piano.lowest + 1 },\r\n (_, index) => piano.lowest + index,\r\n );\r\n const whites = keys.filter((midi) => !BLACK_CHROMAS.has(midi % 12));\r\n const width = whites.length * WHITE_KEY_WIDTH;\r\n const svg = createSvgElement('svg', {\r\n class: 'piano-board',\r\n viewBox: `0 0 ${width + 1} ${WHITE_KEY_HEIGHT + 1}`,\r\n role: 'img',\r\n 'aria-label': `Piano keys for ${chord}: ${piano.midi.map((midi) => getMidiNoteName(midi, keyText)).join(' ')}`,\r\n });\r\n const drawKey = (midi: number, x: number, black: boolean) => {\r\n const lit = piano.midi.includes(midi);\r\n\r\n svg.append(\r\n createSvgElement('rect', {\r\n class: `piano-board__${black ? 'black' : 'white'}${lit ? ' is-lit' : ''}`,\r\n x: x + 0.5,\r\n y: 0.5,\r\n width: black ? BLACK_KEY_WIDTH : WHITE_KEY_WIDTH,\r\n height: black ? BLACK_KEY_HEIGHT : WHITE_KEY_HEIGHT,\r\n rx: 2,\r\n }),\r\n );\r\n\r\n if (lit) {\r\n const label = createSvgElement('text', {\r\n class: 'piano-board__label',\r\n x: x + 0.5 + (black ? BLACK_KEY_WIDTH : WHITE_KEY_WIDTH) / 2,\r\n y: black ? BLACK_KEY_HEIGHT - 6 : WHITE_KEY_HEIGHT - 8,\r\n 'text-anchor': 'middle',\r\n });\r\n\r\n label.textContent = getMidiNoteName(midi, keyText).replace(/-?\\d+$/, '');\r\n svg.append(label);\r\n }\r\n };\r\n\r\n whites.forEach((midi, index) =>\r\n drawKey(midi, index * WHITE_KEY_WIDTH, false),\r\n );\r\n keys\r\n .filter((midi) => BLACK_CHROMAS.has(midi % 12))\r\n .forEach((midi) => {\r\n const whitesBelow = whites.filter((white) => white < midi).length;\r\n\r\n drawKey(midi, whitesBelow * WHITE_KEY_WIDTH - BLACK_KEY_WIDTH / 2, true);\r\n });\r\n\r\n return svg;\r\n}\r\n","import { SVGuitarChord } from 'svguitar';\r\n\r\nimport playerPluginCss from './OpenLyricPluginPlayer.scss?inline';\r\nimport { createPianoBoard } from './chord-figure.ts';\r\nimport { getChordCardMidi, type ChordCard } from './chord-library.ts';\r\nimport {\r\n OpenLyricPlayer,\r\n type OpenLyricPlayerStateEvent,\r\n type OpenLyricPlayerStepEvent,\r\n} from './OpenLyricPlayer.ts';\r\nimport {\r\n getMidiNoteName,\r\n getStrumInstrument,\r\n type StrumInstrumentId,\r\n} from './instruments.ts';\r\nimport type {\r\n OpenLyricChordPopupEventDetail,\r\n OpenLyricComponentHost,\r\n OpenLyricContributions,\r\n OpenLyricPlugin,\r\n OpenLyricRenderContext,\r\n OpenLyricStyleContribution,\r\n OpenLyricSurface,\r\n} from '../../editor/components/index.ts';\r\n\r\n/**\r\n * `OpenLyricPluginPlayer` — the strum player as a plugin for the two preview\r\n * components, `OpenLyric` (`lyric`) and `OpenLyricMarkdownManager`\r\n * (`markdown`):\r\n *\r\n * - every strumming-pattern card gets a play/stop button, and the step\r\n * sounding is marked on the card while it plays;\r\n * - a click on a chord (a key note) rolls it, and so does the ▶ the chord\r\n * popup gets; beside it, a view toggle shows the chord on the instrument\r\n * (its diagram, or piano keys) in place of the popup's guitar diagram. A\r\n * strumming-pattern reference's popup gets a ▶ that plays its pattern;\r\n * - the preview's settings popup gets an instrument picker\r\n * (`contributes.settings`).\r\n *\r\n * Attach one instance per component. Every instance plays through one\r\n * `OpenLyricPlayer` shared by the page unless it is handed its own, so one\r\n * pattern sounds at a time across the embeds.\r\n */\r\n\r\nexport interface OpenLyricPluginPlayerOptions {\r\n /**\r\n * The instrument to play on until a reader picks another in the settings\r\n * popup, and what Reset returns to. Default `guitar`.\r\n */\r\n instrument?: StrumInstrumentId;\r\n /** The player to sound through. Default: one shared by the page. */\r\n player?: OpenLyricPlayer;\r\n /**\r\n * localStorage key remembering the instrument a reader picked. Default\r\n * `openLyricPluginPlayer.instrument`; `null` remembers nothing.\r\n */\r\n storageKey?: string | null;\r\n}\r\n\r\ntype PatternState = 'idle' | 'loading' | 'playing';\r\n\r\n/** A pattern card's play request, read off its data attributes. */\r\ninterface PatternCardRequest {\r\n id: string;\r\n /** The card's `data-pattern-index`. */\r\n index: string;\r\n label: string;\r\n source: string;\r\n key: string;\r\n tempo: number;\r\n time: string;\r\n}\r\n\r\n/** The chord popup's view toggle while it shows a chord of this plugin's. */\r\ninterface ChordView {\r\n chord: string;\r\n button: HTMLElement;\r\n panel: HTMLElement;\r\n /**\r\n * The view's frame in the panel, kept while it shows so the instrument\r\n * arrows survive a redraw: dropping the focused one would blur the popup,\r\n * and a blurred popup closes.\r\n */\r\n shell: {\r\n view: HTMLElement;\r\n caption: HTMLElement;\r\n figure: HTMLElement;\r\n notes: HTMLElement;\r\n } | null;\r\n}\r\n\r\n/** One component this plugin is attached to. */\r\ninterface Session {\r\n /** Prefix of the pattern ids this session plays. */\r\n uid: string;\r\n root: HTMLElement | null;\r\n /** Takes the root's listeners and observer back off. */\r\n unbindRoot: (() => void) | null;\r\n /**\r\n * The ▶ in the chord popup while it shows something of this session's —\r\n * with the pattern card's index when it is a pattern reference's.\r\n */\r\n popupButton: { button: HTMLElement; index: string | null } | null;\r\n /** The last pattern played here, and the instrument it sounds on. */\r\n request: (PatternCardRequest & { instrument: StrumInstrumentId }) | null;\r\n state: PatternState;\r\n}\r\n\r\nconst PLUGIN_ID = 'player';\r\nconst DEFAULT_INSTRUMENT: StrumInstrumentId = 'guitar';\r\nconst DEFAULT_STORAGE_KEY = 'openLyricPluginPlayer.instrument';\r\n/** What a pattern strums when the song has no `Key`. */\r\nconst DEFAULT_KEY = 'C';\r\n/** The playground's default tempo, for a song without a `Tempo`. */\r\nconst DEFAULT_TEMPO = 70;\r\nconst MIN_TEMPO = 20;\r\nconst MAX_TEMPO = 300;\r\nconst DEFAULT_TIME = '4/4';\r\n\r\n/** The marker exports strip (`readExportMarkup` in the core). */\r\nconst PLUGIN_UI_ATTRIBUTE = 'data-ol-plugin-ui';\r\n/** Scopes this plugin's stylesheet to a render root it decorates. */\r\nconst ROOT_MARKER_ATTRIBUTE = 'data-ol-player';\r\nconst CARD_STATE_ATTRIBUTE = 'data-olpp-state';\r\nconst STEP_ATTRIBUTE = 'data-olpp-step';\r\n/** How `PluginHost` marks the style nodes it installs for a plugin. */\r\nconst PLUGIN_STYLE_MARKER = 'data-open-lyric-plugin-id';\r\n\r\nconst PATTERN_CARD_SELECTOR = '[data-pattern-source][data-pattern-index]';\r\nconst CHORD_SELECTOR = '[data-chord-key]';\r\n/** `OPEN_LYRIC_CHORD_POPUP_EVENT` in the core: the chord popup opening. */\r\nconst CHORD_POPUP_EVENT = 'open-lyric:chord-popup';\r\nconst PLAY_BUTTON_SELECTOR = `.olpp-play[${PLUGIN_UI_ATTRIBUTE}=\"${PLUGIN_ID}\"]`;\r\nconst STEP_ROW_SELECTOR =\r\n '.ol-song-view__pattern-row--stroke, .ol-song-view__pattern-row--count';\r\n\r\n/** The zero-width separators a Khmer document may put between groups. */\r\nconst INVISIBLE_SEPARATOR_RE = /[\\u200B-\\u200D\\u2060\\uFEFF]/g;\r\n\r\nconst VIEW_ICON =\r\n '<svg class=\"olpp-view-toggle__icon\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M2 12s3.6-6.5 10-6.5S22 12 22 12s-3.6 6.5-10 6.5S2 12 2 12z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\"/><circle cx=\"12\" cy=\"12\" r=\"3.2\" fill=\"currentColor\"/></svg>';\r\n\r\n/**\r\n * Inlined (`?inline`) rather than contributed as an `href`, so the sheet\r\n * travels inside the bundle and needs no second request.\r\n *\r\n * A `<link href>` pointing at an emitted `.css` asset is the one thing that\r\n * cannot be relied on here: a Vite dev host serves a `.css` under\r\n * `node_modules` as a JavaScript HMR module (`Content-Type: text/javascript`),\r\n * and a browser refuses a stylesheet whose MIME is not `text/css` — so every\r\n * control this sheet styles, the chord popup's ▶ among them, rendered as a\r\n * bare unstyled `<button>`. `cssText` has no MIME to get wrong.\r\n */\r\nconst PLAYER_STYLE: OpenLyricStyleContribution = { cssText: playerPluginCss };\r\n\r\nlet sharedPlayer: OpenLyricPlayer | null = null;\r\nlet sessionCount = 0;\r\nlet fieldCount = 0;\r\n\r\nfunction getSharedPlayer(): OpenLyricPlayer {\r\n sharedPlayer ??= new OpenLyricPlayer();\r\n return sharedPlayer;\r\n}\r\n\r\nfunction isInstrumentId(value: unknown): value is StrumInstrumentId {\r\n return (\r\n typeof value === 'string' && Boolean(getStrumInstrument(value as never))\r\n );\r\n}\r\n\r\nfunction readTempo(text: string | undefined): number {\r\n const match = String(text ?? '').match(/\\d+(?:\\.\\d+)?/);\r\n const tempo = match ? Math.round(Number(match[0])) : Number.NaN;\r\n return Number.isFinite(tempo) && tempo > 0\r\n ? Math.min(MAX_TEMPO, Math.max(MIN_TEMPO, tempo))\r\n : DEFAULT_TEMPO;\r\n}\r\n\r\nfunction readPatternCard(card: HTMLElement, uid: string): PatternCardRequest {\r\n const index = card.dataset['patternIndex'] ?? '';\r\n const number = Number.parseInt(index, 10) + 1;\r\n const numberText = Number.isFinite(number) ? `#${number}` : '';\r\n const label = (card.dataset['patternLabel'] ?? '').trim();\r\n\r\n return {\r\n id: `${uid}:${index}`,\r\n index,\r\n label:\r\n label && label !== `Pattern ${numberText}`\r\n ? `${numberText} ${label}`.trim()\r\n : `Pattern ${numberText}`.trim(),\r\n source: (card.dataset['patternSource'] ?? '')\r\n .replace(INVISIBLE_SEPARATOR_RE, ' ')\r\n .trim(),\r\n key: (card.dataset['patternPreviewKey'] ?? '').trim() || DEFAULT_KEY,\r\n tempo: readTempo(card.dataset['patternPreviewTempo']),\r\n time: (card.dataset['patternPreviewTime'] ?? '').trim() || DEFAULT_TIME,\r\n };\r\n}\r\n\r\nfunction findPatternCards(root: ParentNode): HTMLElement[] {\r\n return Array.from(root.querySelectorAll<HTMLElement>(PATTERN_CARD_SELECTOR));\r\n}\r\n\r\nfunction findPatternCard(root: ParentNode, index: string): HTMLElement | null {\r\n return (\r\n findPatternCards(root).find(\r\n (card) => card.dataset['patternIndex'] === index,\r\n ) ?? null\r\n );\r\n}\r\n\r\nfunction clearSteps(scope: ParentNode): void {\r\n for (const cell of Array.from(\r\n scope.querySelectorAll(`[${STEP_ATTRIBUTE}]`),\r\n )) {\r\n cell.removeAttribute(STEP_ATTRIBUTE);\r\n }\r\n}\r\n\r\nexport class OpenLyricPluginPlayer implements OpenLyricPlugin {\r\n readonly id = PLUGIN_ID;\r\n readonly apiVersion = 1 as const;\r\n readonly surfaces: readonly OpenLyricSurface[] = ['lyric', 'markdown'];\r\n readonly contributes: OpenLyricContributions;\r\n\r\n /**\r\n * Put the plugin's stylesheet in `document.head` for the page's lifetime,\r\n * without attaching the plugin — the page-level counterpart of the sheet an\r\n * attached plugin installs into its component. Idempotent. The sheet only\r\n * styles render roots the plugin has marked, so it changes nothing on a page\r\n * that never attaches it.\r\n */\r\n static installShellStyle(): void {\r\n const head = globalThis.document?.head;\r\n if (!head || !PLAYER_STYLE.cssText) {\r\n return;\r\n }\r\n // One sheet per page: the marker alone identifies it, the stylesheet\r\n // being inline now rather than an href to compare.\r\n if (head.querySelector(`style[${PLUGIN_STYLE_MARKER}=\"${PLUGIN_ID}\"]`)) {\r\n return;\r\n }\r\n const style = Object.assign(document.createElement('style'), {\r\n textContent: PLAYER_STYLE.cssText,\r\n });\r\n style.setAttribute(PLUGIN_STYLE_MARKER, PLUGIN_ID);\r\n head.append(style);\r\n }\r\n\r\n #player: OpenLyricPlayer | null;\r\n #playerUnsubscribes: Array<() => void> = [];\r\n #defaultInstrument: StrumInstrumentId;\r\n #instrument: StrumInstrumentId;\r\n #storageKey: string | null;\r\n #sessions = new Map<OpenLyricComponentHost, Session>();\r\n #instrumentSelects = new Set<HTMLSelectElement>();\r\n /** Whether the chord popup shows the instrument view; kept across popups. */\r\n #chordViewOpen = false;\r\n #chordView: ChordView | null = null;\r\n\r\n constructor(options: OpenLyricPluginPlayerOptions = {}) {\r\n if (\r\n options.instrument !== undefined &&\r\n !isInstrumentId(options.instrument)\r\n ) {\r\n throw new Error(`Unknown instrument “${String(options.instrument)}”.`);\r\n }\r\n this.#defaultInstrument = options.instrument ?? DEFAULT_INSTRUMENT;\r\n this.#storageKey =\r\n options.storageKey === undefined\r\n ? DEFAULT_STORAGE_KEY\r\n : options.storageKey;\r\n // A reader's saved pick outranks the host's configured instrument.\r\n this.#instrument = this.#readStoredInstrument() ?? this.#defaultInstrument;\r\n this.#player = options.player ?? null;\r\n this.contributes = {\r\n style: [PLAYER_STYLE],\r\n settings: [\r\n {\r\n render: (mount) => this.#renderInstrumentField(mount),\r\n reset: () => this.resetInstrument(),\r\n },\r\n ],\r\n };\r\n // `contributes.style` alone is not enough for this plugin, and the reason\r\n // is structural rather than a timing accident: the chord popup is appended\r\n // to `document.body` by the core, NOT into the component container that a\r\n // scoped contribution installs into. A host that never calls\r\n // `installShellStyle()` therefore got a popup ▶ with no rules reaching it —\r\n // a bare unstyled <button>, 16×6 px, its triangle missing because the icon\r\n // is drawn entirely in CSS. So the plugin installs its own page-level sheet\r\n // instead of relying on the host to remember. It is idempotent, and safe to\r\n // install unasked for the same reason `installShellStyle` is: every rule is\r\n // scoped under markup only this plugin writes, so a page that ends up\r\n // attaching nothing is unchanged by it.\r\n OpenLyricPluginPlayer.installShellStyle();\r\n }\r\n\r\n /** The player patterns and chords sound through. */\r\n get player(): OpenLyricPlayer {\r\n this.#player ??= getSharedPlayer();\r\n return this.#player;\r\n }\r\n\r\n /** The instrument patterns and chords play on. */\r\n get instrument(): StrumInstrumentId {\r\n return this.#instrument;\r\n }\r\n\r\n /**\r\n * Switch instruments: remembered, reflected in every settings popup showing\r\n * the picker, and a pattern playing here restarts on the new one.\r\n */\r\n set instrument(next: StrumInstrumentId) {\r\n if (!isInstrumentId(next)) {\r\n throw new Error(`Unknown instrument “${String(next)}”.`);\r\n }\r\n this.#writeStoredInstrument(next);\r\n this.#applyInstrument(next);\r\n }\r\n\r\n /** Back to the configured instrument, forgetting the reader's pick. */\r\n resetInstrument(): void {\r\n this.#writeStoredInstrument(null);\r\n this.#applyInstrument(this.#defaultInstrument);\r\n }\r\n\r\n /** Stop a pattern this plugin started, wherever it plays. */\r\n stop(): void {\r\n for (const session of this.#sessions.values()) {\r\n this.#stopSession(session);\r\n }\r\n }\r\n\r\n /* ------------------------------ lifecycle ------------------------------ */\r\n\r\n install(host: OpenLyricComponentHost): void {\r\n this.#session(host);\r\n }\r\n\r\n uninstall(host: OpenLyricComponentHost): void {\r\n const session = this.#sessions.get(host);\r\n if (session) {\r\n this.#detachSession(session);\r\n this.#sessions.delete(host);\r\n }\r\n if (!this.#sessions.size) {\r\n for (const off of this.#playerUnsubscribes.splice(0)) {\r\n off();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Attached to a preview that is already mounted, nothing has rendered with\r\n * this plugin yet — render once so the cards get their buttons.\r\n */\r\n onMount(host: OpenLyricComponentHost): void {\r\n if (!this.#session(host).root) {\r\n (host as Partial<{ reload(): void }>).reload?.();\r\n }\r\n }\r\n\r\n onUnmount(host: OpenLyricComponentHost): void {\r\n const session = this.#sessions.get(host);\r\n if (session) {\r\n this.#detachSession(session);\r\n }\r\n }\r\n\r\n onAfterRender(\r\n host: OpenLyricComponentHost,\r\n context: OpenLyricRenderContext,\r\n ): void {\r\n const session = this.#session(host);\r\n const root = context.root;\r\n\r\n if (session.root !== root) {\r\n session.unbindRoot?.();\r\n session.root = root;\r\n session.unbindRoot = this.#bindRoot(session, root);\r\n }\r\n this.#decorate(session);\r\n }\r\n\r\n /* ------------------------------- sessions ------------------------------ */\r\n\r\n #session(host: OpenLyricComponentHost): Session {\r\n let session = this.#sessions.get(host);\r\n if (!session) {\r\n sessionCount += 1;\r\n session = {\r\n uid: `olpp-${sessionCount}`,\r\n root: null,\r\n unbindRoot: null,\r\n popupButton: null,\r\n request: null,\r\n state: 'idle',\r\n };\r\n this.#sessions.set(host, session);\r\n }\r\n return session;\r\n }\r\n\r\n /** Stop the session's pattern and take everything added back off its root. */\r\n #detachSession(session: Session): void {\r\n this.#stopSession(session);\r\n session.unbindRoot?.();\r\n session.unbindRoot = null;\r\n session.popupButton?.button.remove();\r\n session.popupButton = null;\r\n\r\n const root = session.root;\r\n if (root) {\r\n root.removeAttribute(ROOT_MARKER_ATTRIBUTE);\r\n for (const button of Array.from(\r\n root.querySelectorAll(PLAY_BUTTON_SELECTOR),\r\n )) {\r\n button.remove();\r\n }\r\n for (const card of Array.from(\r\n root.querySelectorAll(`[${CARD_STATE_ATTRIBUTE}]`),\r\n )) {\r\n card.removeAttribute(CARD_STATE_ATTRIBUTE);\r\n }\r\n clearSteps(root);\r\n }\r\n session.root = null;\r\n }\r\n\r\n #stopSession(session: Session): void {\r\n // Forgetting the request also cancels a play still loading for it: when\r\n // it starts, `#handleState` finds it stale and stops it.\r\n session.request = null;\r\n if (session.state === 'idle') {\r\n return;\r\n }\r\n session.state = 'idle';\r\n if (this.#ownsPattern(session, this.#player?.patternId)) {\r\n this.#player?.stop();\r\n }\r\n this.#paint(session);\r\n }\r\n\r\n #ownsPattern(session: Session, patternId: string | null | undefined) {\r\n return Boolean(patternId?.startsWith(`${session.uid}:`));\r\n }\r\n\r\n /**\r\n * After a re-render: keep a playing pattern in step with its card. An edited\r\n * pattern swaps in mid-run; a changed key, tempo or time restarts it; a card\r\n * that is gone stops it.\r\n */\r\n #followRender(session: Session): void {\r\n const request = session.request;\r\n if (session.state === 'idle' || !request || !session.root) {\r\n return;\r\n }\r\n\r\n const card = findPatternCard(session.root, request.index);\r\n if (!card) {\r\n this.#stopSession(session);\r\n return;\r\n }\r\n\r\n const next = readPatternCard(card, session.uid);\r\n if (\r\n next.key !== request.key ||\r\n next.tempo !== request.tempo ||\r\n next.time !== request.time\r\n ) {\r\n this.#playCard(session, card);\r\n return;\r\n }\r\n if (next.source !== request.source) {\r\n try {\r\n this.player.updatePattern(\r\n this.player.parsePattern(\r\n { id: next.id, label: next.label, source: next.source },\r\n next.time,\r\n ),\r\n );\r\n session.request = { ...request, ...next };\r\n } catch {\r\n this.#stopSession(session);\r\n }\r\n }\r\n }\r\n\r\n /* -------------------------------- player ------------------------------- */\r\n\r\n #ensurePlayer(): OpenLyricPlayer {\r\n const player = this.player;\r\n if (!this.#playerUnsubscribes.length) {\r\n this.#playerUnsubscribes.push(\r\n player.on('state', (event) => this.#handleState(event)),\r\n player.on('step', (event) => this.#handleStep(event)),\r\n );\r\n }\r\n return player;\r\n }\r\n\r\n #handleState({ state, patternId, error }: OpenLyricPlayerStateEvent): void {\r\n let isStale = false;\r\n for (const session of this.#sessions.values()) {\r\n const previous = session.state;\r\n const isOwn = this.#ownsPattern(session, patternId);\r\n if (state !== 'idle' && isOwn && patternId !== session.request?.id) {\r\n // A play this session has since stopped or replaced, starting late\r\n // (its chord tables were still loading): silenced below.\r\n isStale = true;\r\n continue;\r\n }\r\n if (state !== 'idle' && isOwn) {\r\n session.state = state;\r\n } else {\r\n // Anything else ends this session's pattern: a stop, a failed load, or\r\n // another pattern taking over the player. The stop inside a new play\r\n // lands here too, and that play's own `loading` follows at once.\r\n if (error && previous === 'loading') {\r\n console.error('Open Lyric player failed to load a sound.', error);\r\n }\r\n session.state = 'idle';\r\n }\r\n if (session.state !== previous) {\r\n this.#paint(session);\r\n }\r\n }\r\n if (isStale) {\r\n this.#player?.stop();\r\n }\r\n }\r\n\r\n #handleStep({ patternId, stepNumber }: OpenLyricPlayerStepEvent): void {\r\n for (const session of this.#sessions.values()) {\r\n const request = session.request;\r\n if (!request || request.id !== patternId || !session.root) {\r\n continue;\r\n }\r\n const card = findPatternCard(session.root, request.index);\r\n if (!card) {\r\n continue;\r\n }\r\n clearSteps(card);\r\n for (const row of Array.from(card.querySelectorAll(STEP_ROW_SELECTOR))) {\r\n row.children[stepNumber - 1]?.setAttribute(STEP_ATTRIBUTE, '');\r\n }\r\n }\r\n }\r\n\r\n #togglePattern(session: Session, card: HTMLElement): void {\r\n const index = card.dataset['patternIndex'] ?? '';\r\n if (session.state !== 'idle' && session.request?.index === index) {\r\n this.#stopSession(session);\r\n return;\r\n }\r\n this.#playCard(session, card);\r\n }\r\n\r\n /** Play the card's pattern — straight from the click, as audio requires. */\r\n #playCard(session: Session, card: HTMLElement): void {\r\n const request = {\r\n ...readPatternCard(card, session.uid),\r\n instrument: this.#instrument,\r\n };\r\n const player = this.#ensurePlayer();\r\n\r\n session.request = request;\r\n session.state = 'loading';\r\n this.#paint(session);\r\n\r\n player\r\n .playPattern({\r\n pattern: {\r\n id: request.id,\r\n label: request.label,\r\n source: request.source,\r\n },\r\n key: request.key,\r\n tempo: request.tempo,\r\n time: request.time,\r\n instrument: request.instrument,\r\n })\r\n .catch((error: unknown) => {\r\n if (session.request === request && session.state !== 'idle') {\r\n session.state = 'idle';\r\n this.#paint(session);\r\n }\r\n console.error(\r\n `Open Lyric player could not play ${request.label}.`,\r\n error,\r\n );\r\n });\r\n }\r\n\r\n #playChord(chord: string): void {\r\n this.#ensurePlayer()\r\n .playChord(chord, this.#instrument)\r\n .catch((error: unknown) => {\r\n console.warn(`Open Lyric player could not play “${chord}”.`, error);\r\n });\r\n }\r\n\r\n #applyInstrument(next: StrumInstrumentId): void {\r\n for (const select of this.#instrumentSelects) {\r\n select.value = next;\r\n }\r\n if (next === this.#instrument) {\r\n return;\r\n }\r\n this.#instrument = next;\r\n this.#renderChordView();\r\n\r\n for (const session of this.#sessions.values()) {\r\n const card =\r\n session.state !== 'idle' && session.request && session.root\r\n ? findPatternCard(session.root, session.request.index)\r\n : null;\r\n if (card) {\r\n this.#playCard(session, card);\r\n } else {\r\n // The idle buttons name the instrument too.\r\n this.#paint(session);\r\n }\r\n }\r\n }\r\n\r\n /* --------------------------------- DOM --------------------------------- */\r\n\r\n /**\r\n * Give every pattern card on the root its button and mirror the playing\r\n * state onto it. Runs after each render, and whenever the root's content\r\n * changes without one: an app page's incremental fast path patches the\r\n * panel itself.\r\n */\r\n #decorate(session: Session): void {\r\n const root = session.root;\r\n if (!root) {\r\n return;\r\n }\r\n root.setAttribute(ROOT_MARKER_ATTRIBUTE, '');\r\n for (const card of findPatternCards(root)) {\r\n if (!card.querySelector(PLAY_BUTTON_SELECTOR)) {\r\n this.#addPlayButton(session, card);\r\n }\r\n }\r\n this.#followRender(session);\r\n this.#paint(session);\r\n }\r\n\r\n #createPlayButton(onPress: () => void): HTMLButtonElement {\r\n const button = document.createElement('button');\r\n button.type = 'button';\r\n button.className = 'olpp-play';\r\n button.setAttribute(PLUGIN_UI_ATTRIBUTE, PLUGIN_ID);\r\n button.dataset['state'] = 'idle';\r\n button.innerHTML =\r\n '<span class=\"olpp-play__icon\" aria-hidden=\"true\"></span>';\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault();\r\n // The card itself can be a host's click target (the markdown panel\r\n // jumps to the pattern's source line); a press here only plays.\r\n event.stopPropagation();\r\n onPress();\r\n });\r\n return button;\r\n }\r\n\r\n #addPlayButton(session: Session, card: HTMLElement): void {\r\n const button = this.#createPlayButton(() =>\r\n this.#togglePattern(session, card),\r\n );\r\n const slot =\r\n card.querySelector('.ol-song-view__pattern-heading') ??\r\n card.querySelector('.ol-song-view__pattern-header') ??\r\n card;\r\n slot.prepend(button);\r\n }\r\n\r\n /** Chord clicks, the chord popup opening, and content swapped in. */\r\n #bindRoot(session: Session, root: HTMLElement): () => void {\r\n const onClick = (event: Event) => {\r\n const target = event.target instanceof Element ? event.target : null;\r\n const chord = target?.closest<HTMLElement>(CHORD_SELECTOR);\r\n if (!chord || !root.contains(chord)) {\r\n return;\r\n }\r\n const name = (chord.getAttribute('data-chord-key') ?? '').trim();\r\n if (name) {\r\n this.#playChord(name);\r\n }\r\n };\r\n const onPopup = (event: Event) => this.#fillPopupActions(session, event);\r\n const observer =\r\n typeof MutationObserver === 'function'\r\n ? new MutationObserver(() => this.#decorate(session))\r\n : null;\r\n\r\n root.addEventListener('click', onClick);\r\n root.addEventListener(CHORD_POPUP_EVENT, onPopup);\r\n observer?.observe(root, { childList: true, subtree: true });\r\n return () => {\r\n root.removeEventListener('click', onClick);\r\n root.removeEventListener(CHORD_POPUP_EVENT, onPopup);\r\n observer?.disconnect();\r\n };\r\n }\r\n\r\n /**\r\n * The chord popup opened for something in this preview: put a ▶ in its\r\n * header. A chord's rolls the chord; a pattern reference's plays or stops\r\n * the pattern of the card it refers to.\r\n */\r\n #fillPopupActions(session: Session, event: Event): void {\r\n const detail = (event as CustomEvent<OpenLyricChordPopupEventDetail>)\r\n .detail;\r\n const trigger = event.target instanceof Element ? event.target : null;\r\n if (!detail?.actions || !trigger) {\r\n return;\r\n }\r\n\r\n if (detail.kind === 'chord') {\r\n const chord = detail.text;\r\n const button = this.#createPlayButton(() => this.#playChord(chord));\r\n const text = `Play ${chord} on ${this.#instrumentLabel()}`;\r\n button.setAttribute('aria-label', text);\r\n button.title = text;\r\n session.popupButton = { button, index: null };\r\n detail.actions.append(button);\r\n // An older core's popup has no panel to show the view in.\r\n if (detail.panel) {\r\n detail.actions.append(this.#createViewToggle(chord, detail.panel));\r\n this.#renderChordView();\r\n }\r\n return;\r\n }\r\n\r\n const reference = Number.parseInt(\r\n trigger.getAttribute('data-pattern-reference-index') ?? '',\r\n 10,\r\n );\r\n const index = String(reference - 1);\r\n if (!session.root || !findPatternCard(session.root, index)) {\r\n return;\r\n }\r\n const button = this.#createPlayButton(() => {\r\n const card = session.root ? findPatternCard(session.root, index) : null;\r\n if (card) {\r\n this.#togglePattern(session, card);\r\n }\r\n });\r\n session.popupButton = { button, index };\r\n detail.actions.append(button);\r\n this.#paint(session);\r\n }\r\n\r\n /**\r\n * Mirror the session's state onto its cards, their buttons, and a pattern\r\n * reference's ▶ in the chord popup.\r\n */\r\n #paint(session: Session): void {\r\n const root = session.root;\r\n if (!root) {\r\n return;\r\n }\r\n\r\n for (const card of findPatternCards(root)) {\r\n const index = card.dataset['patternIndex'] ?? '';\r\n const state: PatternState =\r\n session.request?.index === index ? session.state : 'idle';\r\n const label = readPatternCard(card, session.uid).label;\r\n\r\n if (state === 'idle') {\r\n card.removeAttribute(CARD_STATE_ATTRIBUTE);\r\n } else {\r\n card.setAttribute(CARD_STATE_ATTRIBUTE, state);\r\n }\r\n if (state !== 'playing') {\r\n clearSteps(card);\r\n }\r\n\r\n const button = card.querySelector<HTMLElement>(PLAY_BUTTON_SELECTOR);\r\n if (button) {\r\n this.#paintButton(button, state, label);\r\n }\r\n if (session.popupButton?.index === index) {\r\n this.#paintButton(session.popupButton.button, state, label);\r\n }\r\n }\r\n }\r\n\r\n #paintButton(button: HTMLElement, state: PatternState, label: string) {\r\n const text =\r\n state === 'idle'\r\n ? `Play ${label} on ${this.#instrumentLabel()}`\r\n : `Stop ${label}`;\r\n button.dataset['state'] = state;\r\n button.setAttribute('aria-pressed', String(state !== 'idle'));\r\n button.setAttribute('aria-label', text);\r\n button.title = text;\r\n }\r\n\r\n #instrumentLabel(): string {\r\n return getStrumInstrument(this.#instrument)?.label ?? this.#instrument;\r\n }\r\n\r\n /* ----------------------------- chord view ------------------------------ */\r\n\r\n /**\r\n * The chord popup's view toggle, beside its ▶: shows the chord on the\r\n * instrument in the popup's panel. It stays on for the next chord until\r\n * pressed again.\r\n */\r\n #createViewToggle(chord: string, panel: HTMLElement): HTMLButtonElement {\r\n const button = document.createElement('button');\r\n button.type = 'button';\r\n button.className = 'olpp-view-toggle';\r\n button.setAttribute(PLUGIN_UI_ATTRIBUTE, PLUGIN_ID);\r\n button.innerHTML = VIEW_ICON;\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n this.#chordViewOpen = !this.#chordViewOpen;\r\n this.#renderChordView();\r\n });\r\n this.#chordView = { chord, button, panel, shell: null };\r\n return button;\r\n }\r\n\r\n /** Mirror the toggle onto the popup open for one of this plugin's chords. */\r\n #renderChordView(): void {\r\n const view = this.#chordView;\r\n if (!view) {\r\n return;\r\n }\r\n if (!view.button.isConnected) {\r\n // The popup has moved on (or closed) since.\r\n this.#chordView = null;\r\n return;\r\n }\r\n const { chord, button, panel } = view;\r\n const text = this.#chordViewOpen\r\n ? `Back to the guitar diagram for ${chord}`\r\n : `Show ${chord} on ${this.#instrumentLabel()}`;\r\n button.setAttribute('aria-pressed', String(this.#chordViewOpen));\r\n button.setAttribute('aria-label', text);\r\n button.title = text;\r\n\r\n if (!this.#chordViewOpen) {\r\n view.shell = null;\r\n panel.replaceChildren();\r\n panel.hidden = true;\r\n return;\r\n }\r\n\r\n const card = this.player.resolveChord(this.#instrument, chord);\r\n if (card) {\r\n this.#drawChordView(card, view);\r\n return;\r\n }\r\n\r\n // The chord tables load on first use.\r\n view.shell = null;\r\n panel.replaceChildren(this.#createViewMessage('Loading…'));\r\n panel.hidden = false;\r\n this.player.loadChords().then(\r\n () => {\r\n if (this.#chordView === view) {\r\n this.#renderChordView();\r\n }\r\n },\r\n (error: unknown) => {\r\n if (this.#chordView === view) {\r\n panel.replaceChildren(\r\n this.#createViewMessage('Unable to load the chord voicings.'),\r\n );\r\n }\r\n console.error('Open Lyric player could not load its chords.', error);\r\n },\r\n );\r\n }\r\n\r\n /** The view's frame: the instrument between its ‹ › arrows, then the figure. */\r\n #createViewShell(): NonNullable<ChordView['shell']> {\r\n const view = document.createElement('div');\r\n view.className = 'olpp-view';\r\n\r\n const switcher = document.createElement('div');\r\n switcher.className = 'olpp-view__switcher';\r\n\r\n const caption = document.createElement('p');\r\n caption.className = 'olpp-view__caption';\r\n caption.setAttribute('aria-live', 'polite');\r\n\r\n const arrow = (step: -1 | 1) => {\r\n const button = document.createElement('button');\r\n button.type = 'button';\r\n button.className = `olpp-view__arrow olpp-view__arrow--${step < 0 ? 'previous' : 'next'}`;\r\n button.setAttribute(PLUGIN_UI_ATTRIBUTE, PLUGIN_ID);\r\n button.setAttribute(\r\n 'aria-label',\r\n step < 0 ? 'Previous instrument' : 'Next instrument',\r\n );\r\n button.title = button.getAttribute('aria-label')!;\r\n button.innerHTML = `<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"${step < 0 ? 'M15 5l-7 7 7 7' : 'M9 5l7 7-7 7'}\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`;\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n this.#stepInstrument(step);\r\n });\r\n return button;\r\n };\r\n\r\n const figure = document.createElement('div');\r\n figure.className = 'olpp-view__figure';\r\n\r\n const notes = document.createElement('p');\r\n notes.className = 'olpp-view__notes';\r\n\r\n switcher.append(arrow(-1), caption, arrow(1));\r\n view.append(switcher, figure, notes);\r\n return { view, caption, figure, notes };\r\n }\r\n\r\n /** Switch to the instrument before or after this one, wrapping around. */\r\n #stepInstrument(step: -1 | 1): void {\r\n const ids = OpenLyricPlayer.instruments.map((instrument) => instrument.id);\r\n const index = ids.indexOf(this.#instrument);\r\n this.instrument = ids[(index + step + ids.length) % ids.length]!;\r\n }\r\n\r\n #drawChordView(card: ChordCard, chordView: ChordView): void {\r\n const { panel } = chordView;\r\n const instrument = this.#instrumentLabel();\r\n const chord = card.chord;\r\n const midi = getChordCardMidi(card);\r\n\r\n if (!chordView.shell?.view.isConnected) {\r\n chordView.shell = this.#createViewShell();\r\n panel.replaceChildren(chordView.shell.view);\r\n }\r\n const { view, caption, figure, notes } = chordView.shell;\r\n view.dataset['kind'] = card.kind;\r\n caption.textContent = instrument;\r\n figure.replaceChildren();\r\n notes.textContent = midi\r\n .map((note) => getMidiNoteName(note, chord).replace(/-?\\d+$/, ''))\r\n .join(' ');\r\n notes.hidden = !midi.length;\r\n\r\n // Shown before drawing: SVGuitar measures its text as it draws.\r\n panel.hidden = false;\r\n\r\n if (card.kind === 'piano') {\r\n figure.append(createPianoBoard(chord, card.piano, chord));\r\n return;\r\n }\r\n if (card.kind === 'none') {\r\n figure.append(\r\n this.#createViewMessage(\r\n card.reason === 'not-a-chord'\r\n ? `“${chord}” can’t be read as a chord.`\r\n : `No ${instrument} voicing for ${chord}.`,\r\n ),\r\n );\r\n return;\r\n }\r\n\r\n // The popup's own diagram colours, read where the popup's theme applies.\r\n const token = (name: string, fallback: string) =>\r\n getComputedStyle(panel).getPropertyValue(name).trim() || fallback;\r\n try {\r\n new SVGuitarChord(figure)\r\n .configure({\r\n ...card.chart.settings,\r\n title: '',\r\n titleBottomMargin: 0,\r\n backgroundColor: 'transparent',\r\n color: token('--chord-popup-diagram-ink', '#213547'),\r\n fretColor: token('--chord-popup-diagram-line', '#52606d'),\r\n stringColor: token('--chord-popup-diagram-line', '#52606d'),\r\n fretLabelColor: token('--chord-popup-diagram-label', '#52606d'),\r\n tuningsColor: token('--chord-popup-diagram-label', '#52606d'),\r\n fingerColor: token('--accent', '#115e59'),\r\n fingerStrokeColor: token('--accent', '#115e59'),\r\n fingerTextColor: token('--surface', '#ffffff'),\r\n svgTitle: `${instrument} diagram for ${chord}`,\r\n })\r\n .chord(card.chart.chord)\r\n .draw();\r\n } catch (error) {\r\n figure.replaceChildren(\r\n this.#createViewMessage('Unable to draw the diagram.'),\r\n );\r\n console.error(`Open Lyric player could not draw ${chord}.`, error);\r\n }\r\n }\r\n\r\n #createViewMessage(text: string): HTMLElement {\r\n const message = document.createElement('p');\r\n message.className = 'olpp-view__message';\r\n message.textContent = text;\r\n return message;\r\n }\r\n\r\n /* ------------------------------- settings ------------------------------ */\r\n\r\n #renderInstrumentField(mount: HTMLElement): () => void {\r\n fieldCount += 1;\r\n const id = `olpp-instrument-${fieldCount}`;\r\n\r\n const label = document.createElement('label');\r\n label.className = 'topbar-settings__label';\r\n label.htmlFor = id;\r\n label.textContent = 'Instrument';\r\n\r\n const select = document.createElement('select');\r\n select.id = id;\r\n select.className = 'topbar-settings__text-input';\r\n for (const instrument of OpenLyricPlayer.instruments) {\r\n select.append(new Option(instrument.label, instrument.id));\r\n }\r\n select.value = this.#instrument;\r\n\r\n const note = document.createElement('p');\r\n note.className = 'topbar-settings__field-note';\r\n note.textContent = 'Plays a chord you click and a strumming pattern’s ▶.';\r\n\r\n const onChange = () => {\r\n if (isInstrumentId(select.value)) {\r\n this.instrument = select.value;\r\n }\r\n };\r\n select.addEventListener('change', onChange);\r\n this.#instrumentSelects.add(select);\r\n mount.append(label, select, note);\r\n\r\n return () => {\r\n select.removeEventListener('change', onChange);\r\n this.#instrumentSelects.delete(select);\r\n };\r\n }\r\n\r\n #readStoredInstrument(): StrumInstrumentId | null {\r\n if (!this.#storageKey) {\r\n return null;\r\n }\r\n try {\r\n const stored = globalThis.localStorage?.getItem(this.#storageKey);\r\n return isInstrumentId(stored) ? stored : null;\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n #writeStoredInstrument(instrument: StrumInstrumentId | null): void {\r\n if (!this.#storageKey) {\r\n return;\r\n }\r\n try {\r\n if (instrument) {\r\n globalThis.localStorage?.setItem(this.#storageKey, instrument);\r\n } else {\r\n globalThis.localStorage?.removeItem(this.#storageKey);\r\n }\r\n } catch {\r\n // Storage unavailable (private mode, a sandbox): the pick lasts the page.\r\n }\r\n }\r\n}\r\n","import { SVGuitarChord } from 'svguitar';\r\n\r\nimport {\r\n getChordCardMidi,\r\n getOpenLyricChordGroups,\r\n type ChordCard,\r\n} from './chord-library.ts';\r\nimport { createPianoBoard } from './chord-figure.ts';\r\nimport { getStrumSound } from './instrument-sounds.ts';\r\nimport { getMidiNoteName, type StrumInstrument } from './instruments.ts';\r\nimport { getChordRollSeconds } from './strum-plan.ts';\r\nimport type { OpenLyricPlayer } from './OpenLyricPlayer.ts';\r\nimport { START_DELAY_SECONDS } from './StrumPlayer.ts';\r\n\r\n/**\r\n * The playground's Chords section: every chord Open Lyric offers. Point at a\r\n * chord (or focus it) and a card shows its voicing on the selected instrument\r\n * — a chord diagram, or piano keys — with a play button that rolls the chord.\r\n * Pressing the chord itself plays it too.\r\n *\r\n * The card stays up while the pointer is on it and goes shortly after the\r\n * pointer leaves.\r\n */\r\n\r\nexport interface ChordLibraryOptions {\r\n /** The playground root: the card mounts here, inside its theme tokens. */\r\n root: HTMLElement;\r\n groups: HTMLElement;\r\n summary: HTMLElement;\r\n /** Unique within the page — the card's id, for `aria-describedby`. */\r\n cardId: string;\r\n player: OpenLyricPlayer;\r\n /** Aborted when the playground goes; drops the document listeners. */\r\n signal: AbortSignal;\r\n getInstrument(): StrumInstrument;\r\n getKey(): string;\r\n onError(message: string): void;\r\n}\r\n\r\nexport interface ChordLibrary {\r\n /** Re-reads the instrument and key into the chords and any open card. */\r\n refresh(): void;\r\n}\r\n\r\nconst HIDE_DELAY_MS = 400;\r\nconst CARD_GAP_PX = 10;\r\nconst VIEWPORT_MARGIN_PX = 8;\r\n/** How long a chord stays lit after its last note starts. */\r\nconst PLAYING_TAIL_MS = 350;\r\n\r\nconst PLAY_ICON =\r\n '<svg class=\"icon-play\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M8.5 5.8v12.4a.8.8 0 0 0 1.2.7l9.6-6.2a.8.8 0 0 0 0-1.4L9.7 5.1a.8.8 0 0 0-1.2.7z\"/></svg><span class=\"spinner\" aria-hidden=\"true\"></span>';\r\n\r\nfunction readToken(element: Element, name: string, fallback: string) {\r\n return getComputedStyle(element).getPropertyValue(name).trim() || fallback;\r\n}\r\n\r\nexport function mountChordLibrary(options: ChordLibraryOptions): ChordLibrary {\r\n const { root, groups, summary, player, signal } = options;\r\n const chips = new Map<string, HTMLButtonElement>();\r\n const flashTimers = new Map<string, ReturnType<typeof setTimeout>>();\r\n const card = document.createElement('div');\r\n const play = document.createElement('button');\r\n const figure = document.createElement('div');\r\n const notes = document.createElement('p');\r\n const message = document.createElement('p');\r\n let activeChip: HTMLButtonElement | null = null;\r\n let hideTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n card.className = 'chord-popup';\r\n card.id = options.cardId;\r\n card.hidden = true;\r\n card.setAttribute('role', 'group');\r\n play.className = 'chord-popup__play';\r\n play.type = 'button';\r\n play.innerHTML = PLAY_ICON;\r\n figure.className = 'chord-popup__figure';\r\n notes.className = 'chord-popup__notes';\r\n message.className = 'chord-popup__message';\r\n card.append(play, figure, notes, message);\r\n root.append(card);\r\n\r\n function resolve(chord: string): ChordCard {\r\n return (\r\n player.resolveChord(options.getInstrument().id, chord) ?? {\r\n kind: 'none',\r\n chord,\r\n reason: 'no-voicing',\r\n }\r\n );\r\n }\r\n\r\n function describeMissing(popup: Extract<ChordCard, { kind: 'none' }>) {\r\n const instrument = options.getInstrument().label;\r\n\r\n return popup.reason === 'not-a-chord'\r\n ? `Open Lyric's chord theory can't read “${popup.chord}” as a chord, so there is nothing to play.`\r\n : `No playable ${instrument} voicing was found for ${popup.chord}.`;\r\n }\r\n\r\n // ——— Chords ———\r\n\r\n function renderGroups() {\r\n groups.replaceChildren(\r\n ...getOpenLyricChordGroups().map((group) => {\r\n const section = document.createElement('div');\r\n const name = document.createElement('h3');\r\n const list = document.createElement('div');\r\n\r\n section.className = 'chord-group';\r\n name.className = 'chord-group__name';\r\n name.textContent = group.name;\r\n list.className = 'chord-group__chords';\r\n list.setAttribute('role', 'group');\r\n list.setAttribute('aria-label', group.name);\r\n\r\n for (const chord of group.chords) {\r\n const chip = document.createElement('button');\r\n\r\n chip.type = 'button';\r\n chip.className = 'chord-chip';\r\n chip.dataset.chord = chord;\r\n chip.textContent = chord;\r\n chips.set(chord, chip);\r\n list.append(chip);\r\n }\r\n\r\n section.append(name, list);\r\n return section;\r\n }),\r\n );\r\n }\r\n\r\n function refreshChips() {\r\n const instrument = options.getInstrument();\r\n let playable = 0;\r\n\r\n for (const [chord, chip] of chips) {\r\n const available = getChordCardMidi(resolve(chord)).length > 0;\r\n\r\n playable += Number(available);\r\n chip.dataset.available = String(available);\r\n chip.setAttribute(\r\n 'aria-label',\r\n available\r\n ? `${chord}, play on ${instrument.label}`\r\n : `${chord}, nothing to play on ${instrument.label}`,\r\n );\r\n }\r\n\r\n summary.textContent = `${playable} of ${chips.size} chords have a voicing on ${instrument.label}.`;\r\n }\r\n\r\n // ——— The card ———\r\n\r\n function renderCard(chord: string) {\r\n const popup = resolve(chord);\r\n const midi = getChordCardMidi(popup);\r\n const instrument = options.getInstrument();\r\n const keyText = options.getKey();\r\n\r\n card.dataset.kind = popup.kind;\r\n card.setAttribute('aria-label', `${chord} on ${instrument.label}`);\r\n figure.replaceChildren();\r\n message.textContent = '';\r\n\r\n if (popup.kind === 'diagram') {\r\n new SVGuitarChord(figure)\r\n .configure({\r\n ...popup.chart.settings,\r\n color: readToken(card, '--diagram-ink', '#1f2328'),\r\n fingerColor: readToken(card, '--diagram-dot', '#3f4650'),\r\n fingerStrokeColor: readToken(card, '--diagram-dot', '#3f4650'),\r\n fingerTextColor: readToken(card, '--diagram-dot-text', '#ffffff'),\r\n svgTitle: `${instrument.label} diagram for ${chord}`,\r\n })\r\n .chord(popup.chart.chord)\r\n .draw();\r\n\r\n if (popup.voicing.source === 'computed') {\r\n message.textContent = 'Voicing computed from the chord’s notes.';\r\n }\r\n } else {\r\n const title = document.createElement('p');\r\n\r\n title.className = 'chord-popup__title';\r\n title.textContent = chord;\r\n figure.append(title);\r\n\r\n if (popup.kind === 'piano') {\r\n figure.append(createPianoBoard(chord, popup.piano, keyText));\r\n } else {\r\n message.textContent = describeMissing(popup);\r\n }\r\n }\r\n\r\n notes.textContent = midi\r\n .map((note) => getMidiNoteName(note, keyText))\r\n .join(' ');\r\n notes.hidden = !midi.length;\r\n message.hidden = !message.textContent;\r\n play.disabled = !midi.length;\r\n play.setAttribute(\r\n 'aria-label',\r\n midi.length\r\n ? `Play ${chord} on ${instrument.label}`\r\n : `Nothing to play for ${chord}`,\r\n );\r\n play.title = play.getAttribute('aria-label')!;\r\n syncPlayButton();\r\n }\r\n\r\n function syncPlayButton() {\r\n play.dataset.state = (activeChip && activeChip.dataset.state) || '';\r\n }\r\n\r\n function placeCard(chip: HTMLElement) {\r\n const anchor = chip.getBoundingClientRect();\r\n const { offsetWidth: width, offsetHeight: height } = card;\r\n let top = anchor.top - height - CARD_GAP_PX;\r\n\r\n if (top < VIEWPORT_MARGIN_PX) {\r\n top = anchor.bottom + CARD_GAP_PX;\r\n }\r\n\r\n const left = Math.max(\r\n VIEWPORT_MARGIN_PX,\r\n Math.min(\r\n anchor.left + anchor.width / 2 - width / 2,\r\n window.innerWidth - width - VIEWPORT_MARGIN_PX,\r\n ),\r\n );\r\n\r\n card.style.top = `${Math.round(top)}px`;\r\n card.style.left = `${Math.round(left)}px`;\r\n }\r\n\r\n function cancelHide() {\r\n if (hideTimer != null) {\r\n clearTimeout(hideTimer);\r\n hideTimer = null;\r\n }\r\n }\r\n\r\n function showCard(chip: HTMLButtonElement) {\r\n cancelHide();\r\n\r\n if (chip !== activeChip || card.hidden) {\r\n activeChip?.classList.remove('is-open');\r\n activeChip?.removeAttribute('aria-describedby');\r\n activeChip = chip;\r\n chip.classList.add('is-open');\r\n chip.setAttribute('aria-describedby', card.id);\r\n // Shown first: SVGuitar measures its title as it draws, and text in a\r\n // hidden card measures zero.\r\n card.hidden = false;\r\n renderCard(chip.dataset.chord!);\r\n }\r\n\r\n placeCard(chip);\r\n }\r\n\r\n function hideCard() {\r\n cancelHide();\r\n card.hidden = true;\r\n activeChip?.classList.remove('is-open');\r\n activeChip?.removeAttribute('aria-describedby');\r\n activeChip = null;\r\n }\r\n\r\n function scheduleHide() {\r\n cancelHide();\r\n hideTimer = setTimeout(hideCard, HIDE_DELAY_MS);\r\n }\r\n\r\n // ——— Playing ———\r\n\r\n function setChipState(chord: string, state: '' | 'loading' | 'playing') {\r\n const chip = chips.get(chord);\r\n\r\n if (chip) {\r\n chip.dataset.state = state;\r\n }\r\n\r\n syncPlayButton();\r\n }\r\n\r\n function flash(\r\n chord: string,\r\n noteCount: number,\r\n instrument: StrumInstrument,\r\n ) {\r\n clearTimeout(flashTimers.get(chord));\r\n setChipState(chord, 'playing');\r\n flashTimers.set(\r\n chord,\r\n setTimeout(\r\n () => {\r\n flashTimers.delete(chord);\r\n setChipState(chord, '');\r\n },\r\n (START_DELAY_SECONDS +\r\n getChordRollSeconds(getStrumSound(instrument.id), noteCount)) *\r\n 1000 +\r\n PLAYING_TAIL_MS,\r\n ),\r\n );\r\n }\r\n\r\n async function playChord(chord: string) {\r\n const midi = getChordCardMidi(resolve(chord));\r\n\r\n if (!midi.length) {\r\n return;\r\n }\r\n\r\n const instrument = options.getInstrument();\r\n\r\n setChipState(chord, 'loading');\r\n\r\n try {\r\n const played = await player.playChord(chord, instrument.id);\r\n\r\n if (played) {\r\n flash(chord, midi.length, instrument);\r\n } else {\r\n setChipState(chord, '');\r\n }\r\n } catch (error) {\r\n setChipState(chord, '');\r\n options.onError(\r\n `Could not play ${chord}: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n }\r\n\r\n // ——— Wiring ———\r\n\r\n function chipFrom(target: EventTarget | null): HTMLButtonElement | null {\r\n return target instanceof Element\r\n ? target.closest<HTMLButtonElement>('.chord-chip')\r\n : null;\r\n }\r\n\r\n groups.addEventListener('pointerover', (event) => {\r\n const chip = chipFrom(event.target);\r\n\r\n if (chip) {\r\n showCard(chip);\r\n }\r\n });\r\n\r\n groups.addEventListener('pointerout', (event) => {\r\n const chip = chipFrom(event.target);\r\n const next = event.relatedTarget;\r\n\r\n if (\r\n chip &&\r\n !(next instanceof Node && (chip.contains(next) || card.contains(next)))\r\n ) {\r\n scheduleHide();\r\n }\r\n });\r\n\r\n groups.addEventListener('focusin', (event) => {\r\n const chip = chipFrom(event.target);\r\n\r\n if (chip) {\r\n showCard(chip);\r\n }\r\n });\r\n\r\n groups.addEventListener('focusout', (event) => {\r\n const next = event.relatedTarget;\r\n\r\n if (!(next instanceof Node && card.contains(next)) && !chipFrom(next)) {\r\n scheduleHide();\r\n }\r\n });\r\n\r\n groups.addEventListener('click', (event) => {\r\n const chip = chipFrom(event.target);\r\n\r\n if (chip) {\r\n showCard(chip);\r\n void playChord(chip.dataset.chord!);\r\n }\r\n });\r\n\r\n card.addEventListener('pointerenter', cancelHide);\r\n card.addEventListener('pointerleave', scheduleHide);\r\n play.addEventListener('click', () => {\r\n if (activeChip) {\r\n void playChord(activeChip.dataset.chord!);\r\n }\r\n });\r\n\r\n document.addEventListener(\r\n 'keydown',\r\n (event) => {\r\n if (event.key === 'Escape' && !card.hidden) {\r\n hideCard();\r\n }\r\n },\r\n { signal },\r\n );\r\n\r\n document.addEventListener(\r\n 'pointerdown',\r\n (event) => {\r\n const target = event.target;\r\n\r\n if (\r\n !card.hidden &&\r\n !(target instanceof Node && card.contains(target)) &&\r\n !chipFrom(target)\r\n ) {\r\n hideCard();\r\n }\r\n },\r\n { signal },\r\n );\r\n\r\n const follow = () => {\r\n if (activeChip && !card.hidden) {\r\n placeCard(activeChip);\r\n }\r\n };\r\n\r\n window.addEventListener('scroll', follow, {\r\n passive: true,\r\n capture: true,\r\n signal,\r\n });\r\n window.addEventListener('resize', follow, { signal });\r\n signal.addEventListener('abort', () => {\r\n cancelHide();\r\n flashTimers.forEach((timer) => clearTimeout(timer));\r\n flashTimers.clear();\r\n });\r\n\r\n renderGroups();\r\n refreshChips();\r\n\r\n return {\r\n refresh() {\r\n refreshChips();\r\n\r\n if (activeChip && !card.hidden) {\r\n renderCard(activeChip.dataset.chord!);\r\n placeCard(activeChip);\r\n }\r\n },\r\n };\r\n}\r\n","/**\r\n * The playground's markup. Every element the code reaches carries a\r\n * `data-olp-ref`; ids exist only for label/ARIA links and are prefixed per\r\n * instance, so two playgrounds can share a page.\r\n */\r\nexport function createPlaygroundMarkup(prefix: string): string {\r\n const id = (name: string) => `${prefix}-${name}`;\r\n\r\n return `\r\n <div class=\"page\">\r\n <header class=\"page-header\">\r\n <div>\r\n <h1>Strum Player</h1>\r\n <p>\r\n Hear an Open Lyric song's strumming patterns — and every chord Open\r\n Lyric offers — on guitars, keyboards, ukuleles and basses. Patterns\r\n are written in the notation's Strumming Patterns syntax.\r\n </p>\r\n </div>\r\n </header>\r\n\r\n <section class=\"panel song-bar\" aria-label=\"Song\">\r\n <label class=\"song-field\">Key <select data-olp-ref=\"key\"></select></label>\r\n <label class=\"song-field\">Time <select data-olp-ref=\"time\"></select></label>\r\n <div class=\"song-field song-field--tempo\">\r\n <label for=\"${id('tempo')}\">Tempo</label>\r\n <input data-olp-ref=\"tempoRange\" type=\"range\" min=\"20\" max=\"300\" step=\"1\" aria-label=\"Tempo slider\" />\r\n <input data-olp-ref=\"tempo\" id=\"${id('tempo')}\" type=\"number\" min=\"20\" max=\"300\" step=\"1\" inputmode=\"numeric\" />\r\n <span>bpm</span>\r\n </div>\r\n </section>\r\n\r\n <div class=\"layout\">\r\n <section class=\"panel instruments\" aria-labelledby=\"${id('instruments')}\">\r\n <h2 id=\"${id('instruments')}\">Instruments</h2>\r\n <div data-olp-ref=\"instruments\" class=\"instrument-list\" role=\"radiogroup\" aria-labelledby=\"${id('instruments')}\"></div>\r\n <div data-olp-ref=\"voicing\" class=\"voicing\" aria-live=\"polite\"></div>\r\n </section>\r\n\r\n <section class=\"panel patterns\" aria-labelledby=\"${id('patterns')}\">\r\n <div class=\"patterns__header\">\r\n <h2 id=\"${id('patterns')}\">Patterns</h2>\r\n <button data-olp-ref=\"restore\" class=\"text-button\" type=\"button\">Restore example patterns</button>\r\n </div>\r\n <div data-olp-ref=\"patterns\" class=\"pattern-list\"></div>\r\n <p data-olp-ref=\"patternsEmpty\" class=\"pattern-list__empty\" hidden>No patterns — add one below.</p>\r\n <details class=\"add-patterns\">\r\n <summary>Add patterns</summary>\r\n <p>\r\n Open Lyric notation, one pattern per line — <code>d</code> down,\r\n <code>u</code> up, <code>x</code> alternating, <code>-</code> rest,\r\n <code>|</code> between groups, and a <code>// label</code> line\r\n above to name it. Each character is one sixteenth note.\r\n </p>\r\n <label class=\"visually-hidden\" for=\"${id('input')}\">Patterns to add</label>\r\n <textarea data-olp-ref=\"input\" id=\"${id('input')}\" spellcheck=\"false\" placeholder=\"// Island strum&#10;d-d-u-u-d-u-d-u-\"></textarea>\r\n <div class=\"add-patterns__actions\">\r\n <button data-olp-ref=\"add\" class=\"primary-button\" type=\"button\">Add</button>\r\n <span data-olp-ref=\"addError\" class=\"add-patterns__error\" role=\"alert\"></span>\r\n </div>\r\n </details>\r\n </section>\r\n </div>\r\n\r\n <p data-olp-ref=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\"></p>\r\n\r\n <section class=\"panel chords\" aria-labelledby=\"${id('chords')}\">\r\n <div class=\"chords__header\">\r\n <h2 id=\"${id('chords')}\">Chords</h2>\r\n <p data-olp-ref=\"chordsSummary\" class=\"chords__summary\"></p>\r\n </div>\r\n <p class=\"chords__intro\">\r\n Every chord Open Lyric offers. Point at one for its voicing on the\r\n selected instrument, then press the play button on the card, or the\r\n chord itself, to hear it rolled from the lowest string up.\r\n </p>\r\n <div data-olp-ref=\"chords\" class=\"chord-groups\"></div>\r\n </section>\r\n\r\n <details class=\"panel notes\">\r\n <summary>How it plays</summary>\r\n <ul>\r\n <li>\r\n Sound: FluidR3 soundfonts, served with this player and heard in a\r\n small room — nylon guitar for the classical guitar and both\r\n ukuleles (General MIDI has no ukulele), and an electric bass for\r\n Bass, whose line carries on small speakers; the Upright Bass is\r\n darker, as the instrument is.\r\n </li>\r\n <li>\r\n Notes: a pattern strums the song key's chord as the instrument\r\n voices it, in standard tuning — the same voicing the chord's card\r\n shows below.\r\n </li>\r\n <li>\r\n Voicings: guitar and ukulele shapes come from the chords-db\r\n database the editor's chord popup uses, or are computed from the\r\n chord's notes when a computed shape is clearly easier; baritone\r\n ukulele shapes are computed. On guitar and baritone the lowest note\r\n is always the chord's bass. Bass plays the root with its fifth and\r\n octave; piano stacks the chord from its root in octave 3.\r\n </li>\r\n <li>\r\n Strokes: each character of a pattern is a sixteenth note at the\r\n song's tempo. Guitar and ukuleles strum in string order — down from\r\n the lowest string, up from the highest, catching only the top\r\n strings. The keyboards play the chord, the right hand alone on an\r\n up stroke, and the organ holds it; the basses pluck the root on a\r\n down stroke and the octave on an up stroke. A string rings until it is struck again; stopping a\r\n pattern damps the strings still ringing.\r\n </li>\r\n <li>\r\n Feel: the bar's first beat is loudest, then the other beats, the\r\n off-beats, and the sixteenths in between. A soft stroke sounds\r\n darker than a hard one, and each lands a little differently, as a\r\n player's would.\r\n </li>\r\n <li>\r\n Chord cards roll the chord string by string, and bass plucks each\r\n note in turn. A name Open Lyric's chord theory can't read (Cmaj2,\r\n Am4, …) has nothing to play.\r\n </li>\r\n </ul>\r\n </details>\r\n </div>\r\n `;\r\n}\r\n","import playgroundCss from './OpenLyricPlayerPlayground.scss?inline';\r\nimport {\r\n CONFIG_ENUM_FIELD_VALUES,\r\n CONFIG_KEY_VALUES,\r\n} from '../../editor/scripts/shared.ts';\r\nimport {\r\n STRUM_INSTRUMENTS,\r\n getMidiNoteName,\r\n getStrumInstrument,\r\n type StrumInstrument,\r\n type StrumInstrumentId,\r\n type StrumVoicing,\r\n} from './instruments.ts';\r\nimport {\r\n OpenLyricPlayer,\r\n type OpenLyricPlayerStateEvent,\r\n} from './OpenLyricPlayer.ts';\r\nimport {\r\n cycleStrumPatternStep,\r\n parseOpenLyricPattern,\r\n readStrumPatternInput,\r\n type StrumPattern,\r\n type StrumPatternDefinition,\r\n type StrumStep,\r\n} from './patterns.ts';\r\nimport { mountChordLibrary, type ChordLibrary } from './playground-chords.ts';\r\nimport { createPlaygroundMarkup } from './playground-markup.ts';\r\n\r\n/**\r\n * `OpenLyricPlayerPlayground` — hear an Open Lyric song's strumming patterns,\r\n * and every chord Open Lyric offers, on guitars, keyboards, ukuleles and\r\n * basses. Patterns are written in the notation's `Strumming Patterns`\r\n * syntax; it opens on the two patterns of the \"Extended Worship Build\" example\r\n * (Key E, 4/4, 70 bpm). Below them, every chord gets a card with its voicing\r\n * and a play button (`playground-chords.ts`).\r\n *\r\n * The UI over an {@link OpenLyricPlayer}. It renders into a root element of\r\n * its own inside `container`, and its styles are scoped to that root.\r\n *\r\n * Settings and patterns persist in localStorage under `storageKey` — a\r\n * convenience only; it starts from the example whenever nothing usable is\r\n * stored.\r\n */\r\n\r\nexport type OpenLyricPlayerTheme = 'light' | 'dark';\r\n\r\nexport interface OpenLyricPlayerPlaygroundOptions {\r\n /** Where the playground renders. Its existing content is left alone. */\r\n container: HTMLElement;\r\n /** A player to share with the host; otherwise the playground makes one. */\r\n player?: OpenLyricPlayer;\r\n /** `light` or `dark`; omitted, the system's colour scheme decides. */\r\n theme?: OpenLyricPlayerTheme;\r\n /**\r\n * localStorage key for the settings and patterns. Default\r\n * `openLyricStrumPlayer.v2`; `null` keeps nothing between visits.\r\n */\r\n storageKey?: string | null;\r\n}\r\n\r\ninterface PlaySettings {\r\n key: string;\r\n time: string;\r\n tempo: number;\r\n instrument: StrumInstrumentId;\r\n patterns: StrumPatternDefinition[];\r\n}\r\n\r\ntype Refs = {\r\n key: HTMLSelectElement;\r\n time: HTMLSelectElement;\r\n tempo: HTMLInputElement;\r\n tempoRange: HTMLInputElement;\r\n instruments: HTMLElement;\r\n voicing: HTMLElement;\r\n patterns: HTMLElement;\r\n patternsEmpty: HTMLElement;\r\n restore: HTMLButtonElement;\r\n input: HTMLTextAreaElement;\r\n add: HTMLButtonElement;\r\n addError: HTMLElement;\r\n status: HTMLElement;\r\n chords: HTMLElement;\r\n chordsSummary: HTMLElement;\r\n};\r\n\r\nconst REF_NAMES: readonly (keyof Refs)[] = [\r\n 'key',\r\n 'time',\r\n 'tempo',\r\n 'tempoRange',\r\n 'instruments',\r\n 'voicing',\r\n 'patterns',\r\n 'patternsEmpty',\r\n 'restore',\r\n 'input',\r\n 'add',\r\n 'addError',\r\n 'status',\r\n 'chords',\r\n 'chordsSummary',\r\n];\r\n\r\nconst DEFAULT_STORAGE_KEY = 'openLyricStrumPlayer.v2';\r\nconst TEMPO_MIN = 20;\r\nconst TEMPO_MAX = 300;\r\nconst KEYS: readonly string[] = CONFIG_KEY_VALUES;\r\nconst TIME_SIGNATURES: readonly string[] = CONFIG_ENUM_FIELD_VALUES.Time;\r\nconst THEMES: readonly OpenLyricPlayerTheme[] = ['light', 'dark'];\r\n\r\n/** The `Strumming Patterns` of `examples/example1.md`. */\r\nconst EXAMPLE_PATTERNS: readonly StrumPatternDefinition[] = [\r\n { id: 'example-1', label: 'Slow rolling pulse', source: 'd--u-- | d-u-d-' },\r\n { id: 'example-2', label: 'Lift on the chorus', source: 'ddu--u | d-d-du' },\r\n];\r\n\r\nconst DEFAULT_SETTINGS: Omit<PlaySettings, 'patterns'> = {\r\n key: 'E',\r\n time: '4/4',\r\n tempo: 70,\r\n instrument: 'guitar',\r\n};\r\n\r\nconst ARROW_SVG =\r\n '<svg viewBox=\"0 0 14 28\" aria-hidden=\"true\"><path d=\"M7 3v21M2.5 19.5 7 24.5l4.5-5\"/></svg>';\r\n\r\nlet instanceCount = 0;\r\n\r\n// ——— The stylesheet: one <style> per page, however many playgrounds ———\r\n//\r\n// Inline (`?inline`), not a <link> to the emitted .css: a Vite dev host serves\r\n// a `.css` under `node_modules` as a JavaScript HMR module, and a browser\r\n// refuses a stylesheet that is not `text/css`. See `OpenLyricPluginPlayer.ts`.\r\n\r\nlet stylesheetUsers = 0;\r\nlet stylesheet: HTMLStyleElement | null = null;\r\n\r\nfunction acquireStylesheet() {\r\n stylesheetUsers += 1;\r\n\r\n if (!stylesheet) {\r\n stylesheet = document.createElement('style');\r\n stylesheet.textContent = playgroundCss;\r\n stylesheet.dataset.olpStylesheet = '';\r\n document.head.append(stylesheet);\r\n }\r\n}\r\n\r\nfunction releaseStylesheet() {\r\n stylesheetUsers = Math.max(0, stylesheetUsers - 1);\r\n\r\n if (!stylesheetUsers) {\r\n stylesheet?.remove();\r\n stylesheet = null;\r\n }\r\n}\r\n\r\n// ——— Settings ———\r\n\r\nfunction copyExamplePatterns(): StrumPatternDefinition[] {\r\n return EXAMPLE_PATTERNS.map((definition) => ({ ...definition }));\r\n}\r\n\r\nfunction clampTempo(value: unknown): number | null {\r\n const tempo = Math.round(Number(value));\r\n\r\n return Number.isFinite(tempo)\r\n ? Math.min(TEMPO_MAX, Math.max(TEMPO_MIN, tempo))\r\n : null;\r\n}\r\n\r\nfunction isUsableDefinition(value: any): value is StrumPatternDefinition {\r\n if (\r\n !value ||\r\n typeof value.id !== 'string' ||\r\n typeof value.label !== 'string' ||\r\n typeof value.source !== 'string'\r\n ) {\r\n return false;\r\n }\r\n\r\n try {\r\n parseOpenLyricPattern(value, '4/4');\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nfunction readSettings(storageKey: string | null): PlaySettings {\r\n let stored: any = null;\r\n\r\n try {\r\n stored = storageKey\r\n ? JSON.parse(localStorage.getItem(storageKey) ?? 'null')\r\n : null;\r\n } catch {\r\n stored = null;\r\n }\r\n\r\n if (!stored || typeof stored !== 'object') {\r\n return { ...DEFAULT_SETTINGS, patterns: copyExamplePatterns() };\r\n }\r\n\r\n const storedPatterns = Array.isArray(stored.patterns)\r\n ? stored.patterns\r\n .filter(isUsableDefinition)\r\n .map(({ id, label, source }: StrumPatternDefinition) => ({\r\n id,\r\n label,\r\n source,\r\n }))\r\n : null;\r\n\r\n return {\r\n key: KEYS.includes(stored.key) ? stored.key : DEFAULT_SETTINGS.key,\r\n time: TIME_SIGNATURES.includes(stored.time)\r\n ? stored.time\r\n : DEFAULT_SETTINGS.time,\r\n tempo: clampTempo(stored.tempo) ?? DEFAULT_SETTINGS.tempo,\r\n instrument:\r\n getStrumInstrument(stored.instrument)?.id ?? DEFAULT_SETTINGS.instrument,\r\n patterns: storedPatterns ?? copyExamplePatterns(),\r\n };\r\n}\r\n\r\nfunction describeStep(step: StrumStep | null): string {\r\n return step ? step.direction : 'rest';\r\n}\r\n\r\nfunction createPlayIcon(): SVGSVGElement[] {\r\n const template = document.createElement('template');\r\n\r\n template.innerHTML =\r\n '<svg class=\"icon-play\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M8.5 5.8v12.4a.8.8 0 0 0 1.2.7l9.6-6.2a.8.8 0 0 0 0-1.4L9.7 5.1a.8.8 0 0 0-1.2.7z\"/></svg>' +\r\n '<svg class=\"icon-stop\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><rect x=\"6.5\" y=\"6.5\" width=\"11\" height=\"11\" rx=\"1.6\"/></svg>';\r\n\r\n return Array.from(template.content.children) as SVGSVGElement[];\r\n}\r\n\r\nfunction fillStep(element: HTMLElement, pattern: StrumPattern, index: number) {\r\n const step = pattern.steps[index];\r\n const count = pattern.counts[index] ?? String(index + 1);\r\n const description = `${count} (step ${index + 1}): ${describeStep(step)}`;\r\n const [stroke, countElement] = Array.from(element.children) as HTMLElement[];\r\n\r\n element.classList.toggle('is-active', Boolean(step));\r\n element.classList.toggle('step--break', pattern.groupBreaks.includes(index));\r\n element.setAttribute('aria-label', `${description}. Press to change.`);\r\n element.title = description;\r\n stroke.innerHTML = step ? ARROW_SVG : '';\r\n stroke.firstElementChild?.classList.toggle(\r\n 'stroke-up',\r\n step?.direction === 'up',\r\n );\r\n countElement.textContent = count;\r\n}\r\n\r\nfunction createStep(pattern: StrumPattern, index: number): HTMLElement {\r\n const element = document.createElement('button');\r\n\r\n element.type = 'button';\r\n element.className = 'step';\r\n element.dataset.step = String(index + 1);\r\n element.dataset.action = 'cycle';\r\n\r\n for (const part of ['stroke', 'count']) {\r\n const child = document.createElement('span');\r\n\r\n child.className = `step__${part}`;\r\n child.setAttribute('aria-hidden', 'true');\r\n element.append(child);\r\n }\r\n\r\n fillStep(element, pattern, index);\r\n return element;\r\n}\r\n\r\nfunction createPatternCard(pattern: StrumPattern): HTMLElement {\r\n const card = document.createElement('article');\r\n const header = document.createElement('div');\r\n const play = document.createElement('button');\r\n const spinner = document.createElement('span');\r\n const name = document.createElement('h3');\r\n const remove = document.createElement('button');\r\n const steps = document.createElement('div');\r\n const source = document.createElement('p');\r\n\r\n card.className = 'pattern-card';\r\n card.dataset.patternId = pattern.id;\r\n card.dataset.state = 'idle';\r\n header.className = 'pattern-card__header';\r\n play.className = 'pattern-card__play';\r\n play.type = 'button';\r\n play.dataset.action = 'toggle';\r\n spinner.className = 'spinner';\r\n spinner.setAttribute('aria-hidden', 'true');\r\n play.append(...createPlayIcon(), spinner);\r\n name.className = 'pattern-card__name';\r\n name.textContent = pattern.label;\r\n remove.className = 'pattern-card__remove';\r\n remove.type = 'button';\r\n remove.dataset.action = 'remove';\r\n remove.textContent = '×';\r\n remove.title = `Remove ${pattern.label}`;\r\n remove.setAttribute('aria-label', `Remove ${pattern.label}`);\r\n header.append(play, name, remove);\r\n\r\n steps.className = 'pattern-card__steps';\r\n steps.setAttribute('role', 'group');\r\n steps.setAttribute('aria-label', `${pattern.label} steps`);\r\n steps.append(...pattern.steps.map((_, index) => createStep(pattern, index)));\r\n\r\n source.className = 'pattern-card__source';\r\n source.append(\r\n Object.assign(document.createElement('code'), {\r\n textContent: pattern.source,\r\n }),\r\n );\r\n card.append(header, steps, source);\r\n\r\n return card;\r\n}\r\n\r\nexport class OpenLyricPlayerPlayground {\r\n /** The player behind the UI — the one passed in, or the playground's own. */\r\n readonly player: OpenLyricPlayer;\r\n\r\n #container: HTMLElement;\r\n #ownsPlayer: boolean;\r\n #storageKey: string | null;\r\n #theme: OpenLyricPlayerTheme | undefined;\r\n #prefix: string;\r\n #root: HTMLElement | null = null;\r\n #refs: Refs | null = null;\r\n #abort: AbortController | null = null;\r\n #unsubscribe: (() => void)[] = [];\r\n #settings: PlaySettings;\r\n #patterns = new Map<string, StrumPattern>();\r\n #currentStep: HTMLElement | null = null;\r\n #chordLibrary: ChordLibrary | null = null;\r\n #idCounter = 0;\r\n\r\n constructor(options: OpenLyricPlayerPlaygroundOptions) {\r\n if (!(options?.container instanceof HTMLElement)) {\r\n throw new TypeError(\r\n 'OpenLyricPlayerPlayground needs a `container` element.',\r\n );\r\n }\r\n\r\n this.#container = options.container;\r\n this.#ownsPlayer = !options.player;\r\n this.player = options.player ?? new OpenLyricPlayer();\r\n this.#storageKey =\r\n options.storageKey === undefined\r\n ? DEFAULT_STORAGE_KEY\r\n : options.storageKey;\r\n this.#theme = this.#checkTheme(options.theme);\r\n this.#prefix = `olp-${++instanceCount}`;\r\n this.#settings = readSettings(this.#storageKey);\r\n }\r\n\r\n /** The root element, once rendered. */\r\n get element(): HTMLElement | null {\r\n return this.#root;\r\n }\r\n\r\n /** `light`, `dark`, or `undefined` when the system decides. */\r\n get theme(): OpenLyricPlayerTheme | undefined {\r\n return this.#theme;\r\n }\r\n\r\n /** Switches the theme; `undefined` hands it back to the system. */\r\n setTheme(theme: OpenLyricPlayerTheme | undefined): void {\r\n this.#theme = this.#checkTheme(theme);\r\n this.#applyTheme();\r\n }\r\n\r\n /**\r\n * Renders into the container. Calling it again renders afresh, keeping the\r\n * settings and patterns.\r\n */\r\n render(): this {\r\n this.#teardown();\r\n acquireStylesheet();\r\n\r\n const root = document.createElement('div');\r\n const abort = new AbortController();\r\n\r\n root.className = 'olp-playground';\r\n root.innerHTML = createPlaygroundMarkup(this.#prefix);\r\n this.#root = root;\r\n this.#abort = abort;\r\n this.#refs = Object.fromEntries(\r\n REF_NAMES.map((name) => {\r\n const element = root.querySelector(`[data-olp-ref=\"${name}\"]`);\r\n\r\n if (!element) {\r\n throw new Error(`The playground markup is missing ${name}.`);\r\n }\r\n\r\n return [name, element];\r\n }),\r\n ) as Refs;\r\n this.#applyTheme();\r\n this.#container.append(root);\r\n\r\n this.#wire(abort.signal);\r\n this.#renderSongControls();\r\n this.#renderInstruments();\r\n this.#renderVoicing();\r\n this.#renderPatterns();\r\n this.#loadChords(abort.signal);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Removes the playground and stops its pattern. A player the playground\r\n * made is destroyed with it; one passed in is left to its owner.\r\n */\r\n destroy(): void {\r\n this.#teardown();\r\n\r\n if (this.#ownsPlayer) {\r\n this.player.destroy();\r\n }\r\n }\r\n\r\n #teardown() {\r\n if (!this.#root) {\r\n return;\r\n }\r\n\r\n if (this.#patterns.has(this.player.patternId ?? '')) {\r\n this.player.stop();\r\n }\r\n\r\n this.#abort?.abort();\r\n this.#unsubscribe.forEach((unsubscribe) => unsubscribe());\r\n this.#unsubscribe = [];\r\n this.#root.remove();\r\n this.#root = null;\r\n this.#refs = null;\r\n this.#abort = null;\r\n this.#chordLibrary = null;\r\n this.#currentStep = null;\r\n releaseStylesheet();\r\n }\r\n\r\n #checkTheme(theme: unknown): OpenLyricPlayerTheme | undefined {\r\n if (theme === undefined || theme === null) {\r\n return undefined;\r\n }\r\n\r\n if (!THEMES.includes(theme as OpenLyricPlayerTheme)) {\r\n throw new TypeError(\r\n `Unknown theme “${String(theme)}” — use 'light' or 'dark'.`,\r\n );\r\n }\r\n\r\n return theme as OpenLyricPlayerTheme;\r\n }\r\n\r\n #applyTheme() {\r\n if (!this.#root) {\r\n return;\r\n }\r\n\r\n if (this.#theme) {\r\n this.#root.dataset.theme = this.#theme;\r\n } else {\r\n delete this.#root.dataset.theme;\r\n }\r\n\r\n // SVGuitar reads the diagram inks once, as it draws.\r\n this.#chordLibrary?.refresh();\r\n }\r\n\r\n #saveSettings() {\r\n if (!this.#storageKey) {\r\n return;\r\n }\r\n\r\n try {\r\n localStorage.setItem(this.#storageKey, JSON.stringify(this.#settings));\r\n } catch {\r\n // Storage is a convenience; the playground works without it.\r\n }\r\n }\r\n\r\n #getInstrument(): StrumInstrument {\r\n return (\r\n getStrumInstrument(this.#settings.instrument) ?? STRUM_INSTRUMENTS[0]\r\n );\r\n }\r\n\r\n #getVoicing(): StrumVoicing | null {\r\n return this.player.getVoicing(\r\n this.#settings.instrument,\r\n this.#settings.key,\r\n );\r\n }\r\n\r\n #setStatus(message: string, tone: 'info' | 'error' = 'info') {\r\n if (this.#refs) {\r\n this.#refs.status.textContent = message;\r\n this.#refs.status.dataset.tone = tone;\r\n }\r\n }\r\n\r\n #loadChords(signal: AbortSignal) {\r\n this.player.loadChords().then(\r\n () => {\r\n if (signal.aborted || !this.#root || !this.#refs) {\r\n return;\r\n }\r\n\r\n this.#renderVoicing();\r\n this.#chordLibrary = mountChordLibrary({\r\n root: this.#root,\r\n groups: this.#refs.chords,\r\n summary: this.#refs.chordsSummary,\r\n cardId: `${this.#prefix}-chord-card`,\r\n player: this.player,\r\n signal,\r\n getInstrument: () => this.#getInstrument(),\r\n getKey: () => this.#settings.key,\r\n onError: (message) => this.#setStatus(message, 'error'),\r\n });\r\n },\r\n (error) => {\r\n if (signal.aborted || !this.#refs) {\r\n return;\r\n }\r\n\r\n this.#refs.voicing.textContent = '';\r\n this.#setStatus(\r\n `Could not load chord voicings: ${error instanceof Error ? error.message : String(error)}`,\r\n 'error',\r\n );\r\n },\r\n );\r\n }\r\n\r\n // ——— Song controls ———\r\n\r\n #renderSongControls() {\r\n const refs = this.#refs!;\r\n const settings = this.#settings;\r\n\r\n refs.key.replaceChildren(\r\n ...KEYS.map((key) => new Option(key, key, false, key === settings.key)),\r\n );\r\n refs.time.replaceChildren(\r\n ...TIME_SIGNATURES.map(\r\n (time) => new Option(time, time, false, time === settings.time),\r\n ),\r\n );\r\n refs.tempo.value = String(settings.tempo);\r\n refs.tempoRange.value = String(settings.tempo);\r\n }\r\n\r\n #setTempo(value: unknown) {\r\n const refs = this.#refs!;\r\n const tempo = clampTempo(value) ?? this.#settings.tempo;\r\n\r\n this.#settings.tempo = tempo;\r\n refs.tempo.value = String(tempo);\r\n refs.tempoRange.value = String(tempo);\r\n }\r\n\r\n // ——— Instruments ———\r\n\r\n #renderInstruments() {\r\n this.#refs!.instruments.replaceChildren(\r\n ...STRUM_INSTRUMENTS.map((instrument) => {\r\n const label = document.createElement('label');\r\n const input = document.createElement('input');\r\n const text = document.createElement('span');\r\n const name = document.createElement('span');\r\n const sound = document.createElement('span');\r\n const toggle = document.createElement('span');\r\n\r\n label.className = 'instrument';\r\n input.type = 'radio';\r\n input.name = `${this.#prefix}-instrument`;\r\n input.value = instrument.id;\r\n input.checked = instrument.id === this.#settings.instrument;\r\n text.className = 'instrument__text';\r\n name.className = 'instrument__name';\r\n name.textContent = instrument.label;\r\n sound.className = 'instrument__sound';\r\n sound.textContent = instrument.soundfont.title;\r\n toggle.className = 'instrument__switch';\r\n toggle.setAttribute('aria-hidden', 'true');\r\n text.append(name, sound);\r\n label.append(input, text, toggle);\r\n return label;\r\n }),\r\n );\r\n }\r\n\r\n #renderVoicing() {\r\n const refs = this.#refs!;\r\n\r\n if (!this.player.chordsLoaded) {\r\n refs.voicing.textContent = 'Loading chord voicings…';\r\n return;\r\n }\r\n\r\n const voicing = this.#getVoicing();\r\n const instrument = this.#getInstrument();\r\n\r\n if (!voicing) {\r\n refs.voicing.textContent = `No ${instrument.label} voicing for the key of ${this.#settings.key}.`;\r\n return;\r\n }\r\n\r\n const summary = document.createElement('div');\r\n const notes = document.createElement('div');\r\n\r\n summary.append('Strums ');\r\n summary.append(\r\n Object.assign(document.createElement('strong'), {\r\n textContent: voicing.chord,\r\n }),\r\n );\r\n\r\n if (voicing.card.kind === 'diagram') {\r\n const { frets, baseFret } = voicing.card.voicing;\r\n\r\n summary.append(' — frets ');\r\n summary.append(\r\n Object.assign(document.createElement('code'), {\r\n textContent: frets\r\n .map((fret) =>\r\n fret < 0 ? 'x' : String(fret === 0 ? 0 : fret + baseFret - 1),\r\n )\r\n .join(' '),\r\n }),\r\n );\r\n }\r\n\r\n notes.className = 'voicing__notes';\r\n notes.append(\r\n ...voicing.midi.map((midi) =>\r\n Object.assign(document.createElement('span'), {\r\n className: 'voicing__note',\r\n textContent: getMidiNoteName(midi, this.#settings.key),\r\n }),\r\n ),\r\n );\r\n refs.voicing.replaceChildren(summary, notes);\r\n }\r\n\r\n // ——— Patterns ———\r\n\r\n #findCard(patternId: string): HTMLElement | null {\r\n return (\r\n this.#refs?.patterns.querySelector<HTMLElement>(\r\n `.pattern-card[data-pattern-id=\"${CSS.escape(patternId)}\"]`,\r\n ) ?? null\r\n );\r\n }\r\n\r\n #renderPatterns() {\r\n const refs = this.#refs!;\r\n\r\n this.#patterns = new Map();\r\n\r\n for (const definition of this.#settings.patterns) {\r\n try {\r\n this.#patterns.set(\r\n definition.id,\r\n parseOpenLyricPattern(definition, this.#settings.time),\r\n );\r\n } catch (error) {\r\n console.warn(`Skipping pattern ${definition.label}:`, error);\r\n }\r\n }\r\n\r\n this.#currentStep = null;\r\n refs.patterns.replaceChildren(\r\n ...Array.from(this.#patterns.values(), createPatternCard),\r\n );\r\n refs.patternsEmpty.hidden = this.#patterns.size > 0;\r\n this.#syncPlaybackState();\r\n }\r\n\r\n #syncPlaybackState() {\r\n const refs = this.#refs;\r\n\r\n if (!refs) {\r\n return;\r\n }\r\n\r\n for (const card of refs.patterns.querySelectorAll<HTMLElement>(\r\n '.pattern-card',\r\n )) {\r\n const isActive = card.dataset.patternId === this.player.patternId;\r\n const state = isActive ? this.player.state : 'idle';\r\n const play = card.querySelector<HTMLButtonElement>(\r\n '.pattern-card__play',\r\n )!;\r\n const label =\r\n this.#patterns.get(card.dataset.patternId!)?.label ?? 'pattern';\r\n\r\n card.dataset.state = state;\r\n play.setAttribute('aria-pressed', String(state !== 'idle'));\r\n play.setAttribute(\r\n 'aria-label',\r\n state === 'idle' ? `Play ${label}` : `Stop ${label}`,\r\n );\r\n play.title = state === 'idle' ? `Play ${label}` : `Stop ${label}`;\r\n }\r\n }\r\n\r\n #showCurrentStep(patternId: string, stepNumber: number | null) {\r\n this.#currentStep?.classList.remove('is-current');\r\n this.#currentStep =\r\n stepNumber == null\r\n ? null\r\n : (this.#findCard(patternId)?.querySelector<HTMLElement>(\r\n `.step[data-step=\"${stepNumber}\"]`,\r\n ) ?? null);\r\n this.#currentStep?.classList.add('is-current');\r\n }\r\n\r\n #startPattern(patternId: string) {\r\n const pattern = this.#patterns.get(patternId);\r\n\r\n if (!pattern) {\r\n return;\r\n }\r\n\r\n if (!this.#getVoicing()) {\r\n this.#setStatus(\r\n this.player.chordsLoaded\r\n ? `No ${this.#getInstrument().label} voicing for the key of ${this.#settings.key}.`\r\n : 'Chord voicings are still loading.',\r\n 'error',\r\n );\r\n return;\r\n }\r\n\r\n this.player\r\n .playPattern({\r\n pattern,\r\n key: this.#settings.key,\r\n tempo: this.#settings.tempo,\r\n time: this.#settings.time,\r\n instrument: this.#settings.instrument,\r\n })\r\n .catch((error) =>\r\n this.#setStatus(\r\n `Could not play: ${error instanceof Error ? error.message : String(error)}`,\r\n 'error',\r\n ),\r\n );\r\n }\r\n\r\n /** Picks up a changed key, time, tempo or instrument without a new press. */\r\n #restartIfPlaying() {\r\n const { patternId } = this.player;\r\n\r\n if (\r\n this.player.state !== 'idle' &&\r\n patternId &&\r\n this.#patterns.has(patternId)\r\n ) {\r\n this.#startPattern(patternId);\r\n }\r\n }\r\n\r\n #cycleStep(patternId: string, index: number) {\r\n const settings = this.#settings;\r\n const position = settings.patterns.findIndex(\r\n (definition) => definition.id === patternId,\r\n );\r\n\r\n if (position === -1) {\r\n return;\r\n }\r\n\r\n const definition = cycleStrumPatternStep(\r\n settings.patterns[position],\r\n index,\r\n settings.time,\r\n );\r\n const pattern = parseOpenLyricPattern(definition, settings.time);\r\n const card = this.#findCard(patternId);\r\n\r\n settings.patterns[position] = definition;\r\n this.#patterns.set(patternId, pattern);\r\n this.#saveSettings();\r\n\r\n // Refill in place, so the pressed step keeps focus.\r\n card\r\n ?.querySelectorAll<HTMLElement>('.step')\r\n .forEach((element, stepIndex) => fillStep(element, pattern, stepIndex));\r\n\r\n const source = card?.querySelector('.pattern-card__source code');\r\n\r\n if (source) {\r\n source.textContent = pattern.source;\r\n }\r\n\r\n this.player.updatePattern(pattern);\r\n }\r\n\r\n #removePattern(patternId: string) {\r\n const pattern = this.#patterns.get(patternId);\r\n\r\n if (this.player.patternId === patternId) {\r\n this.player.stop();\r\n }\r\n\r\n this.#settings.patterns = this.#settings.patterns.filter(\r\n (definition) => definition.id !== patternId,\r\n );\r\n this.#saveSettings();\r\n this.#renderPatterns();\r\n this.#setStatus(pattern ? `Removed ${pattern.label}.` : '');\r\n }\r\n\r\n #createPatternId() {\r\n this.#idCounter += 1;\r\n return `custom-${Date.now().toString(36)}-${this.#idCounter}`;\r\n }\r\n\r\n #createLabeler() {\r\n let next =\r\n this.#settings.patterns.reduce((highest, definition) => {\r\n const match = /^Pattern (\\d+)$/.exec(definition.label);\r\n\r\n return match ? Math.max(highest, Number(match[1])) : highest;\r\n }, 0) + 1;\r\n\r\n return () => `Pattern ${next++}`;\r\n }\r\n\r\n #addPatterns() {\r\n const refs = this.#refs!;\r\n\r\n refs.addError.textContent = '';\r\n\r\n try {\r\n const added = readStrumPatternInput(\r\n refs.input.value,\r\n () => this.#createPatternId(),\r\n this.#createLabeler(),\r\n );\r\n\r\n this.#settings.patterns.push(...added);\r\n this.#saveSettings();\r\n this.#renderPatterns();\r\n refs.input.value = '';\r\n this.#setStatus(\r\n `Added ${added.map((definition) => definition.label).join(', ')}.`,\r\n );\r\n } catch (error) {\r\n refs.addError.textContent =\r\n error instanceof Error ? error.message : String(error);\r\n }\r\n }\r\n\r\n #restoreExamplePatterns() {\r\n const others = this.#settings.patterns.filter(\r\n (definition) =>\r\n !EXAMPLE_PATTERNS.some((example) => example.id === definition.id),\r\n );\r\n\r\n if (\r\n EXAMPLE_PATTERNS.some((example) => example.id === this.player.patternId)\r\n ) {\r\n this.player.stop();\r\n }\r\n\r\n this.#settings.patterns = [...copyExamplePatterns(), ...others];\r\n this.#saveSettings();\r\n this.#renderPatterns();\r\n this.#setStatus('Restored the example patterns.');\r\n }\r\n\r\n // ——— Wiring ———\r\n\r\n #onPlayerState({ state, patternId, error }: OpenLyricPlayerStateEvent) {\r\n this.#syncPlaybackState();\r\n\r\n if (state !== 'playing') {\r\n this.#showCurrentStep('', null);\r\n }\r\n\r\n const pattern = patternId ? this.#patterns.get(patternId) : null;\r\n\r\n // A pattern the host started on a shared player is not ours to report.\r\n if (patternId && !pattern) {\r\n return;\r\n }\r\n\r\n if (state === 'loading') {\r\n this.#setStatus(`Loading ${this.#getInstrument().soundfont.title}…`);\r\n } else if (state === 'playing' && pattern) {\r\n this.#setStatus(\r\n `Playing ${pattern.label} — ${this.#getVoicing()?.chord ?? this.#settings.key} on ${this.#getInstrument().label}, ${this.#settings.tempo} bpm, ${this.#settings.time}.`,\r\n );\r\n } else if (error) {\r\n this.#setStatus(\r\n `Could not play: ${error instanceof Error ? error.message : String(error)}`,\r\n 'error',\r\n );\r\n } else {\r\n this.#setStatus('');\r\n }\r\n }\r\n\r\n #wire(signal: AbortSignal) {\r\n const refs = this.#refs!;\r\n const settings = this.#settings;\r\n const listen = { signal };\r\n\r\n this.#unsubscribe.push(\r\n this.player.on('state', (event) => this.#onPlayerState(event)),\r\n this.player.on('step', ({ patternId, stepNumber }) =>\r\n this.#showCurrentStep(patternId, stepNumber),\r\n ),\r\n );\r\n\r\n refs.patterns.addEventListener(\r\n 'click',\r\n (event) => {\r\n const target = (event.target as Element).closest<HTMLElement>(\r\n '[data-action]',\r\n );\r\n const patternId =\r\n target?.closest<HTMLElement>('.pattern-card')?.dataset.patternId;\r\n\r\n if (!target || !patternId) {\r\n return;\r\n }\r\n\r\n switch (target.dataset.action) {\r\n case 'toggle':\r\n if (\r\n this.player.patternId === patternId &&\r\n this.player.state !== 'idle'\r\n ) {\r\n this.player.stop();\r\n } else {\r\n this.#startPattern(patternId);\r\n }\r\n break;\r\n case 'remove':\r\n this.#removePattern(patternId);\r\n break;\r\n case 'cycle':\r\n this.#cycleStep(patternId, Number(target.dataset.step) - 1);\r\n break;\r\n }\r\n },\r\n listen,\r\n );\r\n\r\n refs.key.addEventListener(\r\n 'change',\r\n () => {\r\n settings.key = refs.key.value;\r\n this.#saveSettings();\r\n this.#renderVoicing();\r\n // Note names are spelled the way the song's key spells them.\r\n this.#chordLibrary?.refresh();\r\n this.#restartIfPlaying();\r\n },\r\n listen,\r\n );\r\n\r\n refs.time.addEventListener(\r\n 'change',\r\n () => {\r\n settings.time = refs.time.value;\r\n this.#saveSettings();\r\n this.#renderPatterns();\r\n this.#restartIfPlaying();\r\n },\r\n listen,\r\n );\r\n\r\n refs.tempoRange.addEventListener(\r\n 'input',\r\n () => this.#setTempo(refs.tempoRange.value),\r\n listen,\r\n );\r\n\r\n for (const input of [refs.tempo, refs.tempoRange]) {\r\n input.addEventListener(\r\n 'change',\r\n () => {\r\n this.#setTempo(input.value);\r\n this.#saveSettings();\r\n this.#restartIfPlaying();\r\n },\r\n listen,\r\n );\r\n }\r\n\r\n refs.instruments.addEventListener(\r\n 'change',\r\n (event) => {\r\n const instrument = getStrumInstrument(\r\n (event.target as HTMLInputElement).value,\r\n );\r\n\r\n if (!instrument) {\r\n return;\r\n }\r\n\r\n settings.instrument = instrument.id;\r\n this.#saveSettings();\r\n this.#renderVoicing();\r\n this.#chordLibrary?.refresh();\r\n\r\n if (this.player.state !== 'idle') {\r\n this.#restartIfPlaying();\r\n } else {\r\n // Warm the soundfont up while the choice is fresh (this is a gesture).\r\n this.player.preload(instrument.id).catch(() => {});\r\n }\r\n },\r\n listen,\r\n );\r\n\r\n refs.add.addEventListener('click', () => this.#addPatterns(), listen);\r\n refs.input.addEventListener(\r\n 'keydown',\r\n (event) => {\r\n if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {\r\n event.preventDefault();\r\n this.#addPatterns();\r\n }\r\n },\r\n listen,\r\n );\r\n refs.restore.addEventListener(\r\n 'click',\r\n () => this.#restoreExamplePatterns(),\r\n listen,\r\n );\r\n }\r\n}\r\n","/**\r\n * `open-lyric-plugin-player`'s own version, read from this package's\r\n * `package.json` — the plugin's version, not the core's (`version` from\r\n * `open-lyric` is that one, and the two bump independently).\r\n *\r\n * See `packages/open-lyric/src/version.ts` for why the manifest is imported\r\n * rather than the number restated, and why the import is a named one.\r\n */\r\n\r\nimport { version as manifestVersion } from '../package.json';\r\n\r\nexport const version: string = manifestVersion;\r\n"],"file":"index.js"}
1
+ {"version":3,"mappings":"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P,EACA,EACA,EACA,EACgB,CAChB,MAAO,CACL,MACA,SAAU,SAAS,IACnB,MACA,YACA,OACF,CACF,CAEA,IAAa,EAAmB,CAC9B,YAAa,EACX,2BACA,yCACA,GACA,yEACF,EACA,YAAa,EACX,2BACA,yCACA,GACA,yEACF,EACA,WAAY,EACV,2BACA,sCACA,GACA,yEACF,EACA,eAAgB,EACd,2BACA,yCACA,GACA,yEACF,EACA,cAAe,EACb,2BACA,gCACA,GACA,yEACF,EACA,MAAO,EACL,2BACA,kCACA,GACA,yEACF,EACA,aAAc,EACZ,2BACA,+BACA,GACA,yEACF,EACA,aAAc,EACZ,2BACA,sCACA,GACA,yEACF,CACF,EAEa,EAAgD,CAC3D,CACE,GAAI,SACJ,MAAO,kBACP,UAAW,EAAiB,WAC9B,EACA,CACE,GAAI,mBACJ,MAAO,mBACP,UAAW,EAAiB,WAC9B,EACA,CACE,GAAI,kBACJ,MAAO,kBACP,UAAW,EAAiB,cAC9B,EACA,CAAE,GAAI,QAAS,MAAO,QAAS,UAAW,EAAiB,UAAW,EACtE,CACE,GAAI,iBACJ,MAAO,iBACP,UAAW,EAAiB,aAC9B,EACA,CAAE,GAAI,QAAS,MAAO,QAAS,UAAW,EAAiB,KAAM,EACjE,CACE,GAAI,UACJ,MAAO,iBACP,UAAW,EAAiB,WAC9B,EACA,CACE,GAAI,mBACJ,MAAO,0BACP,UAAW,EAAiB,WAC9B,EACA,CACE,GAAI,OACJ,MAAO,cACP,UAAW,EAAiB,YAC9B,EACA,CACE,GAAI,eACJ,MAAO,sBACP,UAAW,EAAiB,YAC9B,CACF,EAEM,GAAc,CAClB,IACA,KACA,IACA,KACA,IACA,IACA,KACA,IACA,KACA,IACA,KACA,GACF,EACM,GAAa,CACjB,IACA,KACA,IACA,KACA,IACA,IACA,KACA,IACA,KACA,IACA,KACA,GACF,EAEA,SAAgB,EAAmB,EAAoC,CACrE,OAAO,EAAkB,KAAM,GAAe,EAAW,KAAO,CAAE,GAAK,IACzE,CAOA,SAAgB,GACd,EACA,EACA,EACqB,CACrB,IAAM,EAAM,GAAa,CAAO,EAEhC,GAA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","names":["#context","#master","#room","#getBus","#stopVoice","#forget","#slots","#dampVoice","#groups","#buses","#addDrive","#runtime","#state","#statePatternId","#ensureContext","#ensurePlayer","#ensurePreset","#token","#setState","#start","#chordToken","#ensureEngine","#random","#sound","#playback","#clearUiTimers","#stopTimer","#records","#engine","#playerLoad","#presetLoads","#timer","#tick","#playStep","#showStep","#perform","#uiTimers","#strum","#emit","#databases","#listeners","#databaseLoad","#getInstrument","#defaultInstrument","#storageKey","#instrument","#readStoredInstrument","#player","#renderInstrumentField","#writeStoredInstrument","#applyInstrument","#sessions","#stopSession","#session","#detachSession","#playerUnsubscribes","#bindRoot","#decorate","#ownsPattern","#paint","#playCard","#handleState","#handleStep","#instrumentSelects","#renderChordView","#addPlayButton","#followRender","#createPlayButton","#togglePattern","#playChord","#fillPopupActions","#instrumentLabel","#createViewToggle","#paintButton","#chordViewOpen","#chordView","#drawChordView","#createViewMessage","#stepInstrument","#createViewShell","#container","#ownsPlayer","#theme","#checkTheme","#prefix","#settings","#root","#applyTheme","#teardown","#abort","#refs","#wire","#renderSongControls","#renderInstruments","#renderVoicing","#renderPatterns","#loadChords","#patterns","#unsubscribe","#chordLibrary","#currentStep","#setStatus","#getVoicing","#syncPlaybackState","#findCard","#startPattern","#saveSettings","#idCounter","#createPatternId","#createLabeler","#showCurrentStep","#onPlayerState","#removePattern","#cycleStep","#restartIfPlaying","#setTempo","#addPatterns","#restoreExamplePatterns"],"ignoreList":[],"sources":["../../../src/plugins/OpenLyric/domain/chord-voicings.ts","../../../src/plugins/OpenLyricPlayer/chord-library.ts","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0000_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0040_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0170_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0240_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0250_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0270_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0320_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyricPlayer/assets/webaudiofontdata/sound/0340_FluidR3_GM_sf2_file.js?url","../../../src/plugins/OpenLyric/domain/music-theory.ts","../../../src/plugins/OpenLyricPlayer/instruments.ts","../../../src/plugins/OpenLyricPlayer/patterns.ts","../../../src/plugins/OpenLyricPlayer/instrument-sounds.ts","../../../src/plugins/OpenLyricPlayer/strum-plan.ts","../../../src/plugins/OpenLyricPlayer/StrumSoundEngine.ts","../../../src/plugins/OpenLyricPlayer/webaudiofont.ts","../../../src/plugins/OpenLyricPlayer/assets/webaudiofont/WebAudioFontPlayer.js?url","../../../src/plugins/OpenLyricPlayer/StrumPlayer.ts","../../../src/plugins/OpenLyricPlayer/OpenLyricPlayer.ts","../../../src/plugins/OpenLyricPlayer/chord-figure.ts","../../../src/plugins/OpenLyricPlayer/OpenLyricPluginPlayer.ts","../../../src/plugins/OpenLyricPlayer/playground-chords.ts","../../../src/plugins/OpenLyricPlayer/playground-markup.ts","../../../src/plugins/OpenLyricPlayer/OpenLyricPlayerPlayground.ts","../src/version.ts"],"sourcesContent":["/**\r\n * Chord positions from `@tombatossals/chords-db` (MIT): ranking a chord's\r\n * positions from easiest to hardest and turning one into SVGuitar data. Shared\r\n * by the editor's chord popup (guitar) and the strum player (guitar, ukulele).\r\n *\r\n * A position lists one fret per string, lowest string first (`-1` muted,\r\n * `0` open), relative to `baseFret`; `barres` names the relative frets a\r\n * finger lays across.\r\n */\r\n\r\nexport interface ChordDatabasePosition {\r\n frets: number[];\r\n fingers?: number[];\r\n baseFret?: number;\r\n barres?: number[];\r\n capo?: boolean;\r\n midi?: number[];\r\n}\r\n\r\nexport interface ChordDatabaseEntry {\r\n key: string;\r\n suffix: string;\r\n positions: ChordDatabasePosition[];\r\n}\r\n\r\nexport interface ChordDatabase {\r\n chords: Record<string, ChordDatabaseEntry[]>;\r\n}\r\n\r\nexport interface ChordDatabaseSearch {\r\n datasetRoot: string;\r\n suffixCandidates: string[];\r\n}\r\n\r\nfunction getStringCount(position: ChordDatabasePosition) {\r\n return Array.isArray(position?.frets) ? position.frets.length : 6;\r\n}\r\n\r\nfunction getLowestPitchClass(position: ChordDatabasePosition) {\r\n if (!Array.isArray(position?.midi) || !position.midi.length) {\r\n return null;\r\n }\r\n\r\n const lowestMidi = position.midi[0];\r\n return ((lowestMidi % 12) + 12) % 12;\r\n}\r\n\r\n/** Lower is easier: low on the neck, few barres, a small span, open strings. */\r\nfunction scorePosition(position: ChordDatabasePosition) {\r\n const frets = Array.isArray(position?.frets) ? position.frets : [];\r\n const openStringCount = frets.filter((fret) => fret === 0).length;\r\n const mutedStringCount = frets.filter((fret) => fret < 0).length;\r\n const frettedNotes = frets.filter((fret) => fret > 0);\r\n const fretSpan = frettedNotes.length\r\n ? Math.max(...frettedNotes) - Math.min(...frettedNotes)\r\n : 0;\r\n\r\n return (\r\n Math.max((position.baseFret || 1) - 1, 0) * 12 +\r\n (Array.isArray(position?.barres) ? position.barres.length : 0) * 3 +\r\n fretSpan * 2 +\r\n frettedNotes.length +\r\n mutedStringCount * 0.5 -\r\n openStringCount * 1.5\r\n );\r\n}\r\n\r\n/**\r\n * Easiest first; with a slash chord's bass pitch class, positions whose lowest\r\n * note is that bass come first.\r\n */\r\nfunction rankChordPositions<T extends ChordDatabasePosition>(\r\n positions: T[],\r\n bassPitchClass?: number | null,\r\n): T[] {\r\n if (!Array.isArray(positions) || !positions.length) {\r\n return [];\r\n }\r\n\r\n return positions\r\n .map((position, index) => ({\r\n bassMatches:\r\n typeof bassPitchClass === 'number'\r\n ? getLowestPitchClass(position) === bassPitchClass\r\n : false,\r\n index,\r\n position,\r\n score: scorePosition(position),\r\n }))\r\n .sort((left, right) => {\r\n if (left.bassMatches !== right.bassMatches) {\r\n return Number(right.bassMatches) - Number(left.bassMatches);\r\n }\r\n\r\n if (left.score !== right.score) {\r\n return left.score - right.score;\r\n }\r\n\r\n return left.index - right.index;\r\n })\r\n .map((entry) => entry.position);\r\n}\r\n\r\nfunction deriveBarres(position: ChordDatabasePosition) {\r\n const frets = Array.isArray(position?.frets) ? position.frets : [];\r\n const fingers = Array.isArray(position?.fingers) ? position.fingers : [];\r\n const barres = Array.isArray(position?.barres) ? position.barres : [];\r\n const stringCount = getStringCount(position);\r\n\r\n return barres\r\n .map((barreFret) => {\r\n const matchingEntries = frets\r\n .map((fret, index) => ({\r\n fret,\r\n index,\r\n finger: fingers[index] || 0,\r\n }))\r\n .filter((entry) => entry.fret === barreFret);\r\n\r\n if (!matchingEntries.length) {\r\n return null;\r\n }\r\n\r\n const targetFinger =\r\n matchingEntries.find((entry) => entry.finger > 0)?.finger || 0;\r\n const filteredEntries = targetFinger\r\n ? matchingEntries.filter((entry) => entry.finger === targetFinger)\r\n : matchingEntries;\r\n const fromIndex = filteredEntries[0]?.index;\r\n const toIndex = filteredEntries[filteredEntries.length - 1]?.index;\r\n\r\n if (\r\n typeof fromIndex !== 'number' ||\r\n typeof toIndex !== 'number' ||\r\n fromIndex === toIndex\r\n ) {\r\n return null;\r\n }\r\n\r\n const barreFinger = targetFinger > 0 ? String(targetFinger) : undefined;\r\n\r\n return {\r\n fromString: stringCount - fromIndex,\r\n toString: stringCount - toIndex,\r\n fret: barreFret,\r\n style: 'arc' as const,\r\n text: barreFinger,\r\n };\r\n })\r\n .filter((barre) => barre !== null);\r\n}\r\n\r\n/** SVGuitar's `{ fingers, barres, position }` for a position. */\r\nfunction createSvguitarData(position: ChordDatabasePosition) {\r\n const fingers: [number, number | 'x', string?][] = [];\r\n const stringCount = getStringCount(position);\r\n\r\n for (const [index, fret] of (position?.frets || []).entries()) {\r\n const stringNumber = stringCount - index;\r\n const fingerNumber = position?.fingers?.[index] || 0;\r\n\r\n if (fret < 0) {\r\n fingers.push([stringNumber, 'x']);\r\n continue;\r\n }\r\n\r\n if (fret === 0) {\r\n fingers.push([stringNumber, 0]);\r\n continue;\r\n }\r\n\r\n fingers.push([\r\n stringNumber,\r\n fret,\r\n fingerNumber > 0 ? String(fingerNumber) : undefined,\r\n ]);\r\n }\r\n\r\n return {\r\n fingers,\r\n position: Math.max(position?.baseFret || 1, 1),\r\n barres: deriveBarres(position),\r\n };\r\n}\r\n\r\n/**\r\n * The database entry for the first suffix candidate it has. By default a chord\r\n * it has no suffix for falls back to the plain major entry (what the editor's\r\n * popup shows); `allowMajorFallback: false` returns `null` instead.\r\n */\r\nfunction resolveMatchingChordEntry(\r\n database: ChordDatabase | null | undefined,\r\n search: ChordDatabaseSearch,\r\n { allowMajorFallback = true }: { allowMajorFallback?: boolean } = {},\r\n): ChordDatabaseEntry | null {\r\n const chordEntries = database?.chords?.[search.datasetRoot];\r\n if (!Array.isArray(chordEntries) || !chordEntries.length) {\r\n return null;\r\n }\r\n\r\n for (const suffixCandidate of search.suffixCandidates) {\r\n const matchingEntry = chordEntries.find(\r\n (entry) => entry.suffix === suffixCandidate,\r\n );\r\n\r\n if (matchingEntry) {\r\n return matchingEntry;\r\n }\r\n }\r\n\r\n return allowMajorFallback\r\n ? chordEntries.find((entry) => entry.suffix === 'major') || null\r\n : null;\r\n}\r\n\r\nexport {\r\n createSvguitarData,\r\n deriveBarres,\r\n getLowestPitchClass,\r\n rankChordPositions,\r\n resolveMatchingChordEntry,\r\n scorePosition,\r\n};\r\n","import { get as getTonalChord } from '@tonaljs/chord';\r\nimport { get as getInterval } from '@tonaljs/interval';\r\nimport { chroma as getNoteChroma } from '@tonaljs/note';\r\nimport type { Barre, Chord, ChordSettings, Finger } from 'svguitar';\r\n\r\nimport { CHORD_KEY_GROUPS } from '../../editor/scripts/shared.ts';\r\nimport {\r\n createSvguitarData,\r\n scorePosition,\r\n type ChordDatabase,\r\n type ChordDatabasePosition,\r\n} from '../OpenLyric/domain/chord-voicings.ts';\r\nimport type { StrumInstrumentId } from './instruments.ts';\r\n\r\n/**\r\n * A voicing for every chord Open Lyric offers, on each of the strum player's\r\n * instruments — what a chord card draws and plays, and what a pattern strums\r\n * for the song key. Five instruments voice chords; the others borrow one's\r\n * voicings ({@link getVoicingInstrument}).\r\n *\r\n * - **Guitar and ukulele:** the easiest of the positions\r\n * `@tombatossals/chords-db` (MIT, the database the editor's chord popup\r\n * draws from) has for the chord and the one {@link generateFrettedVoicing}\r\n * computes — only those that really spell the chord ({@link spellsChord}),\r\n * ranked by the popup's `scorePosition`.\r\n * - **Baritone ukulele:** computed by {@link generateFrettedVoicing} on its\r\n * standard tuning.\r\n * - **Bass:** the chord's bass note low on the neck, with its fifth and\r\n * octave above ({@link getBassVoicing}).\r\n * - **Piano:** the chord's notes stacked in close root position from octave 3,\r\n * a slash chord's bass an octave below.\r\n *\r\n * The lowest note of a guitar or baritone voicing is always the chord's bass\r\n * (its root, or a slash chord's bass). A ukulele is re-entrant — its G string\r\n * is tuned above its C — so it has no bass, and any inversion will do.\r\n *\r\n * Chords are read by Tonal, the music theory the editor uses. A name it cannot\r\n * read (`Cmaj2`, `Am4`, …) has no voicing on any instrument.\r\n */\r\n\r\n/** Open-string MIDI notes, string order (lowest-pitched course first, except\r\n * the re-entrant ukulele), in standard tuning. */\r\nexport const STANDARD_TUNINGS = {\r\n guitar: [40, 45, 50, 55, 59, 64], // E2 A2 D3 G3 B3 E4\r\n ukulele: [67, 60, 64, 69], // G4 C4 E4 A4 — re-entrant\r\n 'baritone-ukulele': [50, 55, 59, 64], // D3 G3 B3 E4\r\n bass: [28, 33, 38, 43], // E1 A1 D2 G2\r\n} as const satisfies Record<string, readonly number[]>;\r\n\r\nexport type FrettedInstrumentId = keyof typeof STANDARD_TUNINGS;\r\n\r\n/** The instruments whose voicings the others borrow. */\r\nexport type VoicingInstrumentId = FrettedInstrumentId | 'piano';\r\n\r\n/**\r\n * The instrument whose voicings `instrument` plays: a classical or electric\r\n * guitar voices a chord as a guitar does, an electric piano or organ as a\r\n * piano, an upright bass as a bass.\r\n */\r\nexport function getVoicingInstrument(\r\n instrument: StrumInstrumentId,\r\n): VoicingInstrumentId {\r\n switch (instrument) {\r\n case 'classical-guitar':\r\n case 'electric-guitar':\r\n return 'guitar';\r\n case 'electric-piano':\r\n case 'organ':\r\n return 'piano';\r\n case 'upright-bass':\r\n return 'bass';\r\n default:\r\n return instrument;\r\n }\r\n}\r\n\r\nexport interface ChordDatabases {\r\n guitar: ChordDatabase;\r\n ukulele: ChordDatabase;\r\n}\r\n\r\nexport interface OpenLyricChordGroup {\r\n name: string;\r\n chords: string[];\r\n}\r\n\r\n/** What Open Lyric makes of a chord name. */\r\nexport interface ChordSpelling {\r\n name: string;\r\n rootChroma: number;\r\n /** The lowest note: a slash chord's bass, else the root. */\r\n bassChroma: number;\r\n /** Every pitch class of the chord, bass included. */\r\n chromas: number[];\r\n /** Pitch classes a voicing must sound — all but the perfect fifth and inner\r\n * extensions a player may leave out. */\r\n requiredChromas: number[];\r\n /** The chord's intervals above the root, in semitones, low to high. */\r\n semitones: number[];\r\n}\r\n\r\nexport interface FrettedVoicing {\r\n /** One fret per string, string order; `-1` muted, relative to `baseFret`. */\r\n frets: number[];\r\n baseFret: number;\r\n /** MIDI notes in string order, muted strings left out. */\r\n midi: number[];\r\n /** Relative frets a finger lays across. */\r\n barres: number[];\r\n /** chords-db fingerings, one per string (0 = none); computed voicings have none. */\r\n fingers?: number[];\r\n source: 'chords-db' | 'computed';\r\n}\r\n\r\nexport interface PianoVoicing {\r\n /** The keys played, low to high. */\r\n midi: number[];\r\n /** The first and last key of the board drawn. */\r\n lowest: number;\r\n highest: number;\r\n}\r\n\r\nexport type ChordCard =\r\n | {\r\n kind: 'diagram';\r\n chord: string;\r\n voicing: FrettedVoicing;\r\n chart: { chord: Chord; settings: Partial<ChordSettings> };\r\n }\r\n | { kind: 'piano'; chord: string; piano: PianoVoicing }\r\n | {\r\n kind: 'none';\r\n chord: string;\r\n /** `not-a-chord`: Tonal cannot read the name. `no-voicing`: nothing\r\n * playable was found for this instrument. */\r\n reason: 'not-a-chord' | 'no-voicing';\r\n };\r\n\r\nconst PIANO_ROOT_OCTAVE_MIDI = 48; // C3\r\nconst MAX_FRET = 12;\r\nconst FRET_SPAN = 3;\r\nconst MAX_FINGERS = 4;\r\nconst MUTED_STRING_COST = 1.5;\r\n/**\r\n * How much easier a computed voicing must score to beat a chords-db position,\r\n * whose shapes are curated and come with fingerings. Half a fret up the neck\r\n * scores 6.\r\n */\r\nconst DATABASE_PREFERENCE = 6;\r\n\r\n/**\r\n * Open Lyric's chord list (`CHORD_KEY_GROUPS`, the chords its editor\r\n * suggests), with each `F# / Gb` pair split into its two spellings.\r\n */\r\nexport function getOpenLyricChordGroups(): OpenLyricChordGroup[] {\r\n const keyGroups: readonly { name: string; keys: readonly string[] }[] =\r\n CHORD_KEY_GROUPS;\r\n\r\n return keyGroups.map((group) => ({\r\n name: group.name,\r\n chords: group.keys.flatMap((key) =>\r\n key.split('/').map((spelling) => spelling.trim()),\r\n ),\r\n }));\r\n}\r\n\r\nfunction mod12(value: number) {\r\n return ((value % 12) + 12) % 12;\r\n}\r\n\r\n/**\r\n * Whether an interval is one a voicing may drop: the perfect fifth, and any\r\n * 9th, 11th or 13th below the chord's top extension.\r\n */\r\nfunction isOptionalInterval(name: string, isTop: boolean) {\r\n const interval = getInterval(name);\r\n\r\n if (interval.empty) {\r\n return false;\r\n }\r\n\r\n return (\r\n (interval.simple === 5 && interval.q === 'P') ||\r\n (!isTop && [9, 11, 13].includes(interval.num))\r\n );\r\n}\r\n\r\n/** `C#m7/G#` → its root, bass and pitch classes, or `null` if unreadable. */\r\nexport function spellChord(chord: string): ChordSpelling | null {\r\n const name = String(chord ?? '').trim();\r\n const [head, bass] = name.split('/');\r\n const tonal = getTonalChord(head);\r\n const rootChroma = getNoteChroma(tonal.tonic ?? '');\r\n\r\n if (tonal.empty || !tonal.tonic || !Number.isFinite(rootChroma)) {\r\n return null;\r\n }\r\n\r\n const bassChroma = bass ? getNoteChroma(bass) : rootChroma;\r\n\r\n if (!Number.isFinite(bassChroma)) {\r\n return null;\r\n }\r\n\r\n const semitones = tonal.intervals.map(\r\n (interval) => getInterval(interval).semitones ?? 0,\r\n );\r\n const intervalChromas = semitones.map((step) => mod12(rootChroma + step));\r\n const required = tonal.intervals\r\n .filter(\r\n (interval, index) =>\r\n !isOptionalInterval(interval, index === tonal.intervals.length - 1),\r\n )\r\n .map((interval) =>\r\n mod12(rootChroma + (getInterval(interval).semitones ?? 0)),\r\n );\r\n\r\n return {\r\n name,\r\n rootChroma,\r\n bassChroma,\r\n chromas: [...new Set([bassChroma, ...intervalChromas])],\r\n requiredChromas: [...new Set([bassChroma, ...required])],\r\n semitones,\r\n };\r\n}\r\n\r\n/**\r\n * A voicing spells a chord when it sounds nothing outside it, sounds every\r\n * required note and — unless `requireBass` is off — has the chord's bass as\r\n * its lowest note.\r\n */\r\nexport function spellsChord(\r\n midi: readonly number[],\r\n spelling: ChordSpelling,\r\n { requireBass = true }: { requireBass?: boolean } = {},\r\n): boolean {\r\n if (!midi.length) {\r\n return false;\r\n }\r\n\r\n const sounding = new Set(midi.map(mod12));\r\n\r\n return (\r\n [...sounding].every((chroma) => spelling.chromas.includes(chroma)) &&\r\n spelling.requiredChromas.every((chroma) => sounding.has(chroma)) &&\r\n (!requireBass || mod12(Math.min(...midi)) === spelling.bassChroma)\r\n );\r\n}\r\n\r\n/**\r\n * How hard a voicing is to play, lower being easier: the chord popup's\r\n * `scorePosition`, plus a cost for every muted string — a strummed chord\r\n * should ring on as many strings as it can.\r\n */\r\nfunction scoreVoicing(position: ChordDatabasePosition) {\r\n const muted = position.frets.filter((fret) => fret < 0).length;\r\n\r\n return scorePosition(position) + muted * MUTED_STRING_COST;\r\n}\r\n\r\n/** MIDI notes of a position, string order, muted strings left out. */\r\nexport function getPositionMidi(\r\n tuning: readonly number[],\r\n frets: readonly number[],\r\n baseFret: number,\r\n): number[] {\r\n const offset = Math.max(baseFret, 1) - 1;\r\n\r\n return frets.flatMap((fret, index) =>\r\n fret < 0 ? [] : [tuning[index] + (fret === 0 ? 0 : fret + offset)],\r\n );\r\n}\r\n\r\nfunction* combine(\r\n options: readonly (readonly number[])[],\r\n index = 0,\r\n chosen: number[] = [],\r\n): Generator<number[]> {\r\n if (index === options.length) {\r\n yield [...chosen];\r\n return;\r\n }\r\n\r\n for (const option of options[index]) {\r\n chosen.push(option);\r\n yield* combine(options, index + 1, chosen);\r\n chosen.pop();\r\n }\r\n}\r\n\r\n/**\r\n * The easiest playable voicing of `spelling` on a fretted instrument, found by\r\n * trying every fingering in each four-fret window of the neck:\r\n *\r\n * - at least `minNotes` strings sound, each a chord note, next to each\r\n * other so the chord can be strummed;\r\n * - the voicing spells the chord ({@link spellsChord}, with `requireBass`);\r\n * - no more than four fingers, a barre across the lowest fret counting as one.\r\n *\r\n * Ranked like chords-db positions ({@link scoreVoicing}); `null` when nothing\r\n * fits.\r\n */\r\nexport function generateFrettedVoicing(\r\n tuning: readonly number[],\r\n spelling: ChordSpelling,\r\n {\r\n requireBass = true,\r\n minNotes = Math.min(3, tuning.length),\r\n }: { requireBass?: boolean; minNotes?: number } = {},\r\n): FrettedVoicing | null {\r\n let best: { frets: number[]; score: number } | null = null;\r\n\r\n for (let low = 1; low + FRET_SPAN <= MAX_FRET; low += 1) {\r\n const window = [\r\n 0,\r\n ...Array.from({ length: FRET_SPAN + 1 }, (_, offset) => low + offset),\r\n ];\r\n const options = tuning.map((open) => [\r\n -1,\r\n ...window.filter((fret) => spelling.chromas.includes(mod12(open + fret))),\r\n ]);\r\n\r\n for (const frets of combine(options)) {\r\n const first = frets.findIndex((fret) => fret >= 0);\r\n const last = frets.findLastIndex((fret) => fret >= 0);\r\n const sounding = last - first + 1;\r\n\r\n if (\r\n first === -1 ||\r\n sounding < minNotes ||\r\n frets.slice(first, last + 1).includes(-1)\r\n ) {\r\n continue;\r\n }\r\n\r\n const fretted = frets.filter((fret) => fret > 0);\r\n const lowestFret = Math.min(...fretted);\r\n const fingers =\r\n fretted.length > MAX_FINGERS\r\n ? fretted.filter((fret) => fret !== lowestFret).length + 1\r\n : fretted.length;\r\n\r\n if (\r\n fingers > MAX_FINGERS ||\r\n !spellsChord(getPositionMidi(tuning, frets, 1), spelling, {\r\n requireBass,\r\n })\r\n ) {\r\n continue;\r\n }\r\n\r\n const score = scoreVoicing(toPosition(frets));\r\n\r\n if (!best || score < best.score) {\r\n best = { frets, score };\r\n }\r\n }\r\n }\r\n\r\n if (!best) {\r\n return null;\r\n }\r\n\r\n const position = toPosition(best.frets);\r\n\r\n return {\r\n frets: position.frets,\r\n baseFret: position.baseFret!,\r\n midi: getPositionMidi(tuning, best.frets, 1),\r\n barres: position.barres ?? [],\r\n source: 'computed',\r\n };\r\n}\r\n\r\n/** Absolute frets → a chords-db-style position, relative to its base fret. */\r\nfunction toPosition(frets: readonly number[]): ChordDatabasePosition {\r\n const fretted = frets.filter((fret) => fret > 0);\r\n const highest = fretted.length ? Math.max(...fretted) : 0;\r\n const lowest = fretted.length ? Math.min(...fretted) : 0;\r\n const baseFret = highest <= 4 ? 1 : lowest;\r\n const relative = frets.map((fret) => (fret > 0 ? fret - baseFret + 1 : fret));\r\n const relativeFretted = relative.filter((fret) => fret > 0);\r\n\r\n return {\r\n frets: relative,\r\n baseFret,\r\n barres:\r\n relativeFretted.length > MAX_FINGERS\r\n ? [Math.min(...relativeFretted)]\r\n : [],\r\n };\r\n}\r\n\r\n/** A voicing's frets counted from the nut. */\r\nfunction toAbsoluteFrets(voicing: FrettedVoicing): number[] {\r\n return voicing.frets.map((fret) =>\r\n fret > 0 ? fret + voicing.baseFret - 1 : fret,\r\n );\r\n}\r\n\r\n/** chords-db names its roots `C`, `Csharp`/`Db`, …; find the one for a chroma. */\r\nfunction getDatabaseRoot(database: ChordDatabase, chroma: number) {\r\n return Object.keys(database.chords).find(\r\n (root) => getNoteChroma(root.replace('sharp', '#')) === chroma,\r\n );\r\n}\r\n\r\n/** The easiest chords-db position that spells the chord, or `null`. */\r\nexport function findDatabaseVoicing(\r\n database: ChordDatabase,\r\n tuning: readonly number[],\r\n spelling: ChordSpelling,\r\n { requireBass = true }: { requireBass?: boolean } = {},\r\n): (FrettedVoicing & { score: number }) | null {\r\n const root = getDatabaseRoot(database, spelling.rootChroma);\r\n let best: (FrettedVoicing & { score: number }) | null = null;\r\n\r\n for (const entry of root ? database.chords[root] : []) {\r\n for (const position of entry.positions) {\r\n const baseFret = position.baseFret ?? 1;\r\n const midi = getPositionMidi(tuning, position.frets, baseFret);\r\n\r\n if (!spellsChord(midi, spelling, { requireBass })) {\r\n continue;\r\n }\r\n\r\n const score = scoreVoicing(position);\r\n\r\n if (!best || score < best.score) {\r\n best = {\r\n frets: position.frets,\r\n baseFret,\r\n midi,\r\n barres: position.barres ?? [],\r\n fingers: position.fingers,\r\n source: 'chords-db',\r\n score,\r\n };\r\n }\r\n }\r\n }\r\n\r\n return best;\r\n}\r\n\r\n/**\r\n * A bass line for the chord: its bass note on the lowest string that reaches\r\n * it within the first four frets, then — for a chord with a perfect fifth and\r\n * no slash bass — the fifth two frets up on the next string, and the octave\r\n * two frets up on the string after that.\r\n */\r\nexport function getBassVoicing(spelling: ChordSpelling): FrettedVoicing {\r\n const tuning = STANDARD_TUNINGS.bass;\r\n const string = tuning.findIndex(\r\n (open) => mod12(spelling.bassChroma - open) <= 4,\r\n );\r\n const fret = mod12(spelling.bassChroma - tuning[string]);\r\n const withFifth =\r\n spelling.bassChroma === spelling.rootChroma &&\r\n spelling.semitones.includes(7);\r\n const frets = tuning.map((_, index) => {\r\n if (index === string) {\r\n return fret;\r\n }\r\n\r\n if (index === string + 2 || (withFifth && index === string + 1)) {\r\n return fret + 2;\r\n }\r\n\r\n return -1;\r\n });\r\n const position = toPosition(frets);\r\n\r\n return {\r\n frets: position.frets,\r\n baseFret: position.baseFret!,\r\n midi: getPositionMidi(tuning, frets, 1),\r\n barres: position.barres ?? [],\r\n source: 'computed',\r\n };\r\n}\r\n\r\nconst voicingCache = new WeakMap<\r\n ChordDatabases,\r\n Map<string, FrettedVoicing | null>\r\n>();\r\n\r\n/** The voicing a fretted instrument plays for `spelling`, or `null`. */\r\nexport function getFrettedVoicing(\r\n instrument: FrettedInstrumentId,\r\n spelling: ChordSpelling,\r\n databases: ChordDatabases,\r\n): FrettedVoicing | null {\r\n let cache = voicingCache.get(databases);\r\n\r\n if (!cache) {\r\n cache = new Map();\r\n voicingCache.set(databases, cache);\r\n }\r\n\r\n const cacheKey = `${instrument}|${spelling.name}`;\r\n\r\n if (!cache.has(cacheKey)) {\r\n cache.set(cacheKey, findFrettedVoicing(instrument, spelling, databases));\r\n }\r\n\r\n return cache.get(cacheKey)!;\r\n}\r\n\r\nfunction findFrettedVoicing(\r\n instrument: FrettedInstrumentId,\r\n spelling: ChordSpelling,\r\n databases: ChordDatabases,\r\n): FrettedVoicing | null {\r\n if (instrument === 'bass') {\r\n return getBassVoicing(spelling);\r\n }\r\n\r\n const tuning = STANDARD_TUNINGS[instrument];\r\n const requireBass = instrument !== 'ukulele';\r\n const fromDatabase =\r\n instrument === 'guitar' || instrument === 'ukulele'\r\n ? findDatabaseVoicing(databases[instrument], tuning, spelling, {\r\n requireBass,\r\n })\r\n : null;\r\n // A ukulele strums all four strings and a guitar at least four when a shape\r\n // allows; otherwise — and on a baritone ukulele, whose open-position shapes\r\n // often leave the low D out — three will do.\r\n const computed =\r\n (instrument === 'baritone-ukulele'\r\n ? null\r\n : generateFrettedVoicing(tuning, spelling, {\r\n requireBass,\r\n minNotes: 4,\r\n })) ?? generateFrettedVoicing(tuning, spelling, { requireBass });\r\n\r\n if (\r\n fromDatabase &&\r\n (!computed ||\r\n fromDatabase.score - DATABASE_PREFERENCE <=\r\n scoreVoicing(toPosition(toAbsoluteFrets(computed))))\r\n ) {\r\n const { score: _score, ...voicing } = fromDatabase;\r\n\r\n return voicing;\r\n }\r\n\r\n return computed;\r\n}\r\n\r\n/** The chord in close root position from octave 3, slash bass below. */\r\nexport function getPianoVoicing(spelling: ChordSpelling): PianoVoicing {\r\n const root = PIANO_ROOT_OCTAVE_MIDI + spelling.rootChroma;\r\n const midi = spelling.semitones.map((step) => root + step);\r\n\r\n if (spelling.bassChroma !== spelling.rootChroma) {\r\n midi.unshift(PIANO_ROOT_OCTAVE_MIDI - 12 + spelling.bassChroma);\r\n }\r\n\r\n const sorted = [...new Set(midi)].sort((left, right) => left - right);\r\n const lowest = sorted[0] - mod12(sorted[0]);\r\n const top = sorted[sorted.length - 1];\r\n\r\n return {\r\n midi: sorted,\r\n lowest,\r\n highest: top - mod12(top) + 11,\r\n };\r\n}\r\n\r\n/** SVGuitar data and settings for a fretted voicing. */\r\nexport function getChordChart(\r\n instrument: FrettedInstrumentId,\r\n chord: string,\r\n voicing: FrettedVoicing,\r\n): { chord: Chord; settings: Partial<ChordSettings> } {\r\n const data = createSvguitarData(voicing);\r\n\r\n return {\r\n chord: {\r\n fingers: data.fingers as Finger[],\r\n barres: data.barres as Barre[],\r\n },\r\n settings: {\r\n title: chord,\r\n strings: voicing.frets.length,\r\n frets: 4,\r\n position: voicing.baseFret,\r\n fixedDiagramPosition: true,\r\n tuning: [],\r\n strokeWidth: 1.5,\r\n fingerSize: 0.7,\r\n fretLabelFontSize: 40,\r\n titleFontSize: 48,\r\n sidePadding: instrument === 'guitar' ? 0.2 : 0.3,\r\n },\r\n };\r\n}\r\n\r\n/** What a chord card shows, and plays, for `chord` on `instrument`. */\r\nexport function resolveChordCard(\r\n instrument: StrumInstrumentId,\r\n chord: string,\r\n databases: ChordDatabases,\r\n): ChordCard {\r\n const name = String(chord ?? '').trim();\r\n const spelling = spellChord(name);\r\n\r\n if (!spelling) {\r\n return { kind: 'none', chord: name, reason: 'not-a-chord' };\r\n }\r\n\r\n const family = getVoicingInstrument(instrument);\r\n\r\n if (family === 'piano') {\r\n return { kind: 'piano', chord: name, piano: getPianoVoicing(spelling) };\r\n }\r\n\r\n const voicing = getFrettedVoicing(family, spelling, databases);\r\n\r\n return voicing\r\n ? {\r\n kind: 'diagram',\r\n chord: name,\r\n voicing,\r\n chart: getChordChart(family, name, voicing),\r\n }\r\n : { kind: 'none', chord: name, reason: 'no-voicing' };\r\n}\r\n\r\n/** The notes a card plays, in the order they are struck; empty for none. */\r\nexport function getChordCardMidi(card: ChordCard): number[] {\r\n switch (card.kind) {\r\n case 'diagram':\r\n return card.voicing.midi;\r\n case 'piano':\r\n return card.piano.midi;\r\n default:\r\n return [];\r\n }\r\n}\r\n\r\nlet databaseLoad: Promise<ChordDatabases> | null = null;\r\n\r\n/** Loads the chords-db guitar and ukulele tables once, on demand. */\r\nexport function loadChordDatabases(): Promise<ChordDatabases> {\r\n databaseLoad ??= Promise.all([\r\n import('@tombatossals/chords-db/lib/guitar.json'),\r\n import('@tombatossals/chords-db/lib/ukulele.json'),\r\n ]).then(([guitar, ukulele]) => ({\r\n guitar: ((guitar as any).default ?? guitar) as ChordDatabase,\r\n ukulele: ((ukulele as any).default ?? ukulele) as ChordDatabase,\r\n }));\r\n\r\n databaseLoad.catch(() => {\r\n databaseLoad = null;\r\n });\r\n\r\n return databaseLoad;\r\n}\r\n","export default \"__VITE_ASSET__$oUiuAXR7gZMKRQU56HW2w__\"","export default \"__VITE_ASSET__WPGYjwIlzWVNM1bYhOc83w__\"","export default \"__VITE_ASSET__DUq8YoikZLXEejpNZ1KQvg__\"","export default \"__VITE_ASSET__VRAku6fjghkApIISiBWPzg__\"","export default \"__VITE_ASSET__8HSUfh7cJHNFxCkInNzNXw__\"","export default \"__VITE_ASSET__FKNdLzKnlftW3t_gzo81yw__\"","export default \"__VITE_ASSET__P4DPfwIRieI8P6q8r4JZRw__\"","export default \"__VITE_ASSET__cn7IAAGY294pWGc7MpElVg__\"","import { get as getChord, tokenize as tokenizeChord } from '@tonaljs/chord';\r\nimport {\r\n fromSemitones as intervalFromSemitones,\r\n semitones as intervalSemitones,\r\n} from '@tonaljs/interval';\r\nimport { majorKey as getMajorKey, minorKey as getMinorKey } from '@tonaljs/key';\r\nimport {\r\n chroma as getNoteChroma,\r\n enharmonic as getEnharmonicNote,\r\n pitchClass as getPitchClass,\r\n simplify as simplifyNote,\r\n transpose as transposeNote,\r\n} from '@tonaljs/note';\r\n\r\nconst CHORD_DATABASE_ROOTS_BY_CHROMA = [\r\n 'C',\r\n 'Csharp',\r\n 'D',\r\n 'Eb',\r\n 'E',\r\n 'F',\r\n 'Fsharp',\r\n 'G',\r\n 'Ab',\r\n 'A',\r\n 'Bb',\r\n 'B',\r\n];\r\n\r\nfunction normalizeMusicTheoryText(value) {\r\n return String(value ?? '')\r\n .trim()\r\n .replace(/♯/g, '#')\r\n .replace(/♭/g, 'b');\r\n}\r\n\r\nfunction normalizeChordText(value) {\r\n return normalizeMusicTheoryText(value).replace(/\\s+/g, '');\r\n}\r\n\r\nfunction normalizePitchClass(value) {\r\n return getPitchClass(normalizeMusicTheoryText(value)) || '';\r\n}\r\n\r\nfunction getPitchClassChroma(value) {\r\n const chroma = getNoteChroma(value);\r\n return Number.isFinite(chroma) ? chroma : null;\r\n}\r\n\r\nfunction parseMainKey(keyText) {\r\n const normalizedKeyText = normalizeMusicTheoryText(keyText);\r\n\r\n if (!normalizedKeyText) {\r\n return null;\r\n }\r\n\r\n const match = normalizedKeyText.match(/^([A-Ga-g](?:#|b)?)(m)?$/);\r\n if (!match) {\r\n return null;\r\n }\r\n\r\n const tonic = normalizePitchClass(match[1]);\r\n if (!tonic) {\r\n return null;\r\n }\r\n\r\n const isMinor = match[2] === 'm';\r\n const keyData = isMinor ? getMinorKey(tonic) : getMajorKey(tonic);\r\n const chroma = getPitchClassChroma(keyData.tonic);\r\n\r\n if (!keyData.tonic || chroma == null) {\r\n return null;\r\n }\r\n\r\n return {\r\n raw: `${keyData.tonic}${isMinor ? 'm' : ''}`,\r\n tonic: keyData.tonic,\r\n chroma,\r\n isMinor,\r\n alteration: Number.isFinite(keyData.alteration) ? keyData.alteration : 0,\r\n keySignature: String(keyData.keySignature || ''),\r\n };\r\n}\r\n\r\nfunction normalizeStructureKeyOption(value) {\r\n const parsedKey = parseMainKey(value);\r\n return parsedKey ? parsedKey.raw : normalizeMusicTheoryText(value);\r\n}\r\n\r\nfunction createPitchClassCandidates(chroma) {\r\n const normalizedChroma = ((Number(chroma) % 12) + 12) % 12;\r\n const intervalName = intervalFromSemitones(normalizedChroma);\r\n const flatCandidate = normalizePitchClass(\r\n simplifyNote(transposeNote('C', intervalName)),\r\n );\r\n const sharpCandidate = normalizePitchClass(\r\n getEnharmonicNote(flatCandidate) || flatCandidate,\r\n );\r\n\r\n return {\r\n flatCandidate,\r\n sharpCandidate: sharpCandidate || flatCandidate,\r\n };\r\n}\r\n\r\nfunction choosePreferredPitchClass(chroma, targetKey) {\r\n const { flatCandidate, sharpCandidate } = createPitchClassCandidates(chroma);\r\n\r\n if (!flatCandidate) {\r\n return '';\r\n }\r\n\r\n if (!sharpCandidate || sharpCandidate === flatCandidate) {\r\n return flatCandidate;\r\n }\r\n\r\n if (!targetKey) {\r\n return flatCandidate;\r\n }\r\n\r\n if (targetKey.alteration > 0 || targetKey.raw.includes('#')) {\r\n return sharpCandidate;\r\n }\r\n\r\n if (targetKey.alteration < 0 || targetKey.raw.includes('b')) {\r\n return flatCandidate;\r\n }\r\n\r\n if (!/[#b]/.test(flatCandidate)) {\r\n return flatCandidate;\r\n }\r\n\r\n if (!/[#b]/.test(sharpCandidate)) {\r\n return sharpCandidate;\r\n }\r\n\r\n return flatCandidate;\r\n}\r\n\r\nfunction transposePitchClass(noteText, semitoneOffset, targetKeyText) {\r\n const pitchClass = normalizePitchClass(noteText);\r\n const pitchClassChroma = getPitchClassChroma(pitchClass);\r\n\r\n if (!pitchClass || pitchClassChroma == null) {\r\n return normalizeMusicTheoryText(noteText);\r\n }\r\n\r\n return (\r\n choosePreferredPitchClass(\r\n pitchClassChroma + Number(semitoneOffset),\r\n parseMainKey(targetKeyText),\r\n ) || pitchClass\r\n );\r\n}\r\n\r\nfunction transposeChordSymbol(chordText, semitoneOffset, targetKeyText) {\r\n const normalizedChordText = normalizeChordText(chordText);\r\n\r\n if (!normalizedChordText) {\r\n return normalizedChordText;\r\n }\r\n\r\n const [root = '', suffix = '', bassNote = ''] =\r\n tokenizeChord(normalizedChordText);\r\n\r\n if (!root) {\r\n return normalizedChordText;\r\n }\r\n\r\n const nextRoot = transposePitchClass(root, semitoneOffset, targetKeyText);\r\n const nextBassNote = bassNote\r\n ? transposePitchClass(bassNote, semitoneOffset, targetKeyText)\r\n : '';\r\n\r\n return `${nextRoot}${suffix}${nextBassNote ? `/${nextBassNote}` : ''}`;\r\n}\r\n\r\nfunction simplifyChordSymbol(chordText) {\r\n const normalizedChordText = normalizeChordText(chordText);\r\n\r\n if (!normalizedChordText) {\r\n return normalizedChordText;\r\n }\r\n\r\n const [root = '', rawSuffix = ''] = tokenizeChord(normalizedChordText);\r\n\r\n if (!root) {\r\n return normalizedChordText;\r\n }\r\n\r\n const normalizedSuffixCandidate = normalizeChordSuffixCandidate(rawSuffix);\r\n let simplifiedSuffix = rawSuffix;\r\n\r\n if (normalizedSuffixCandidate === 'minor') {\r\n simplifiedSuffix = 'm';\r\n } else if (\r\n /^m(?:6|7|9|11|13)$/.test(normalizedSuffixCandidate) ||\r\n normalizedSuffixCandidate === 'm7b5' ||\r\n /^mmaj(?:7|9|11|13|7b5)$/.test(normalizedSuffixCandidate)\r\n ) {\r\n simplifiedSuffix = 'm';\r\n }\r\n\r\n return `${root}${simplifiedSuffix}`;\r\n}\r\n\r\n/**\r\n * The seven diatonic triads of a song key, tonic first — the chords a song in\r\n * that key most often reaches for (`E` → `E F#m G#m A B C#m D#dim`). Minor\r\n * keys use the natural minor scale. An unparseable key yields `[]`.\r\n */\r\nfunction getKeyDiatonicTriads(keyText) {\r\n const key = parseMainKey(keyText);\r\n\r\n if (!key) {\r\n return [];\r\n }\r\n\r\n const triads = key.isMinor\r\n ? getMinorKey(key.tonic).natural.triads\r\n : getMajorKey(key.tonic).triads;\r\n\r\n return Array.from(triads || [], (triad) => String(triad || '')).filter(\r\n Boolean,\r\n );\r\n}\r\n\r\nfunction createChordTransposition(sourceKeyText, targetKeyText) {\r\n const sourceKey = parseMainKey(sourceKeyText);\r\n const targetKey = parseMainKey(targetKeyText || sourceKeyText);\r\n\r\n if (!sourceKey || !targetKey) {\r\n return {\r\n isActive: false,\r\n semitoneOffset: 0,\r\n intervalName: intervalFromSemitones(0),\r\n targetKeyText: normalizeMusicTheoryText(targetKeyText || sourceKeyText),\r\n };\r\n }\r\n\r\n const semitoneOffset = (targetKey.chroma - sourceKey.chroma + 12 * 8) % 12;\r\n\r\n return {\r\n isActive: semitoneOffset !== 0 || sourceKey.raw !== targetKey.raw,\r\n semitoneOffset,\r\n intervalName: intervalFromSemitones(semitoneOffset),\r\n targetKeyText: targetKey.raw,\r\n };\r\n}\r\n\r\nfunction selectKeyValueForChroma(keyValues, chroma, isMinor) {\r\n const matchingKeys = (keyValues || [])\r\n .map((value) => parseMainKey(value))\r\n .filter((parsedKey) => {\r\n return (\r\n parsedKey &&\r\n parsedKey.isMinor === isMinor &&\r\n parsedKey.chroma === ((Number(chroma) % 12) + 12) % 12\r\n );\r\n })\r\n .sort((left, right) => {\r\n const alterationDifference =\r\n Math.abs(left.alteration) - Math.abs(right.alteration);\r\n if (alterationDifference) {\r\n return alterationDifference;\r\n }\r\n\r\n const preferredAlterationSign = isMinor ? -1 : 1;\r\n const leftPreferred = left.alteration === preferredAlterationSign;\r\n const rightPreferred = right.alteration === preferredAlterationSign;\r\n if (leftPreferred !== rightPreferred) {\r\n return Number(rightPreferred) - Number(leftPreferred);\r\n }\r\n\r\n return left.raw.localeCompare(right.raw);\r\n });\r\n\r\n return matchingKeys[0]?.raw || '';\r\n}\r\n\r\nfunction getAdjacentChromaticKey(keyText, stepOffset, keyValues) {\r\n const parsedKey = parseMainKey(keyText);\r\n\r\n if (!parsedKey) {\r\n return normalizeStructureKeyOption(keyText);\r\n }\r\n\r\n return (\r\n selectKeyValueForChroma(\r\n keyValues,\r\n parsedKey.chroma + Number(stepOffset),\r\n parsedKey.isMinor,\r\n ) || parsedKey.raw\r\n );\r\n}\r\n\r\nfunction normalizeChordSuffixCandidate(value) {\r\n const normalizedValue = normalizeChordText(value);\r\n\r\n if (!normalizedValue) {\r\n return 'major';\r\n }\r\n\r\n if (normalizedValue === '-') {\r\n return 'minor';\r\n }\r\n\r\n if (/^(?:M|Maj|maj)$/i.test(normalizedValue)) {\r\n return 'major';\r\n }\r\n\r\n if (/^(?:Min|Minor|min|minor)$/i.test(normalizedValue)) {\r\n return 'minor';\r\n }\r\n\r\n const leadingMajorMatch = normalizedValue.match(/^M(.+)$/);\r\n if (leadingMajorMatch) {\r\n return `maj${leadingMajorMatch[1].toLowerCase()}`;\r\n }\r\n\r\n const namedMajorMatch = normalizedValue.match(/^Maj(.+)$/i);\r\n if (namedMajorMatch) {\r\n return `maj${namedMajorMatch[1].toLowerCase()}`;\r\n }\r\n\r\n const namedMinorMatch = normalizedValue.match(/^min(?:or)?(.+)$/i);\r\n if (namedMinorMatch) {\r\n return `m${namedMinorMatch[1].toLowerCase()}`;\r\n }\r\n\r\n const mixedMajorMinorMatch = normalizedValue.match(/^m(?:Maj|M)(.+)$/i);\r\n if (mixedMajorMinorMatch) {\r\n return `mmaj${mixedMajorMinorMatch[1].toLowerCase()}`;\r\n }\r\n\r\n if (/^(?:sus|Sus)$/i.test(normalizedValue)) {\r\n return 'sus4';\r\n }\r\n\r\n if (/^(?:7sus|7Sus)$/i.test(normalizedValue)) {\r\n return '7sus4';\r\n }\r\n\r\n if (/^(?:o|O|°)$/u.test(normalizedValue)) {\r\n return 'dim';\r\n }\r\n\r\n if (/^(?:o7|O7|°7)$/u.test(normalizedValue)) {\r\n return 'dim7';\r\n }\r\n\r\n if (/^(?:ø|ø7|Ø|Ø7)$/u.test(normalizedValue)) {\r\n return 'm7b5';\r\n }\r\n\r\n if (/^\\+$/u.test(normalizedValue) || /^augmented$/i.test(normalizedValue)) {\r\n return 'aug';\r\n }\r\n\r\n const augmentedMatch = normalizedValue.match(/^\\+(.+)$/);\r\n if (augmentedMatch) {\r\n return `aug${augmentedMatch[1].toLowerCase()}`;\r\n }\r\n\r\n if (normalizedValue === '5') {\r\n return 'major';\r\n }\r\n\r\n return normalizedValue.toLowerCase();\r\n}\r\n\r\nfunction extractChordDegreeHint(suffixCandidate) {\r\n const normalizedSuffixCandidate =\r\n normalizeChordSuffixCandidate(suffixCandidate);\r\n\r\n if (!normalizedSuffixCandidate) {\r\n return [];\r\n }\r\n\r\n if (/^(2|4|5|6|7|9|11|13)$/.test(normalizedSuffixCandidate)) {\r\n return [normalizedSuffixCandidate];\r\n }\r\n\r\n const qualityDegreeMatch = normalizedSuffixCandidate.match(\r\n /^(?:maj|m|dim|aug|add|no|omit)(2|4|5|6|7|9|11|13)$/,\r\n );\r\n if (qualityDegreeMatch) {\r\n return [qualityDegreeMatch[1]];\r\n }\r\n\r\n const suspensionMatch = normalizedSuffixCandidate.match(/^sus(2|4)$/);\r\n if (suspensionMatch) {\r\n return [suspensionMatch[1]];\r\n }\r\n\r\n const dominantSuspensionMatch =\r\n normalizedSuffixCandidate.match(/^7sus(2|4)$/);\r\n if (dominantSuspensionMatch) {\r\n return ['7', dominantSuspensionMatch[1]];\r\n }\r\n\r\n if (normalizedSuffixCandidate === 'm7b5') {\r\n return ['7'];\r\n }\r\n\r\n return [];\r\n}\r\n\r\nfunction createDatabaseSuffixCandidates(rawSuffix, chordInfo) {\r\n const candidates = [];\r\n\r\n function push(value) {\r\n const normalizedValue = normalizeChordSuffixCandidate(value);\r\n if (!normalizedValue || candidates.includes(normalizedValue)) {\r\n return;\r\n }\r\n\r\n candidates.push(normalizedValue);\r\n }\r\n\r\n push(rawSuffix);\r\n\r\n if (!normalizeChordText(rawSuffix)) {\r\n push('major');\r\n }\r\n\r\n if (chordInfo && !chordInfo.empty) {\r\n const [, symbolSuffix = ''] = tokenizeChord(chordInfo.symbol || '');\r\n push(symbolSuffix);\r\n chordInfo.aliases.forEach(push);\r\n\r\n if (chordInfo.quality === 'Major' && chordInfo.intervals.length === 3) {\r\n push('major');\r\n }\r\n\r\n if (chordInfo.quality === 'Minor' && chordInfo.intervals.length === 3) {\r\n push('minor');\r\n push('m');\r\n }\r\n\r\n if (chordInfo.quality === 'Augmented') {\r\n push('aug');\r\n }\r\n\r\n if (chordInfo.quality === 'Diminished') {\r\n push('dim');\r\n }\r\n }\r\n\r\n push('major');\r\n\r\n return candidates;\r\n}\r\n\r\nfunction createChordDegreeHints(chordKey) {\r\n const normalizedChordKey = normalizeChordText(chordKey);\r\n const [root = '', rawSuffix = ''] = tokenizeChord(normalizedChordKey);\r\n\r\n if (!root) {\r\n return [];\r\n }\r\n\r\n const chordInfo = getChord(normalizedChordKey);\r\n const hints = [];\r\n\r\n function pushHint(value) {\r\n if (!value || hints.includes(value)) {\r\n return;\r\n }\r\n\r\n hints.push(value);\r\n }\r\n\r\n createDatabaseSuffixCandidates(rawSuffix, chordInfo).forEach((candidate) => {\r\n extractChordDegreeHint(candidate).forEach(pushHint);\r\n });\r\n\r\n if (hints.length || !chordInfo || chordInfo.empty) {\r\n return hints;\r\n }\r\n\r\n chordInfo.intervals.forEach((intervalName) => {\r\n const degreeMatch = String(intervalName).match(/^(2|4|6|7|9|11|13)/);\r\n if (degreeMatch) {\r\n pushHint(degreeMatch[1]);\r\n }\r\n });\r\n\r\n return hints;\r\n}\r\n\r\nfunction createChordDatabaseSearchData(chordKey) {\r\n const normalizedChordKey = normalizeChordText(chordKey);\r\n const [root = '', rawSuffix = '', bassRoot = ''] =\r\n tokenizeChord(normalizedChordKey);\r\n\r\n if (!root) {\r\n return null;\r\n }\r\n\r\n const normalizedRoot = normalizePitchClass(root);\r\n const rootChroma = getPitchClassChroma(normalizedRoot);\r\n\r\n if (!normalizedRoot || rootChroma == null) {\r\n return null;\r\n }\r\n\r\n const normalizedBassRoot = normalizePitchClass(bassRoot);\r\n const bassPitchClass = normalizedBassRoot\r\n ? getPitchClassChroma(normalizedBassRoot)\r\n : null;\r\n const chordInfo = getChord(normalizedChordKey);\r\n\r\n return {\r\n bassPitchClass,\r\n bassRoot: normalizedBassRoot,\r\n chordInfo,\r\n datasetRoot: CHORD_DATABASE_ROOTS_BY_CHROMA[rootChroma] || '',\r\n normalizedChordKey,\r\n rawSuffix,\r\n root: normalizedRoot,\r\n suffixCandidates: createDatabaseSuffixCandidates(rawSuffix, chordInfo),\r\n };\r\n}\r\n\r\nfunction describeChordKey(chordKey) {\r\n const normalizedChordKey = normalizeChordText(chordKey);\r\n\r\n if (!normalizedChordKey) {\r\n return 'Chord';\r\n }\r\n\r\n const chordInfo = getChord(normalizedChordKey);\r\n if (!chordInfo.empty && chordInfo.name) {\r\n return `Chord ${chordInfo.name}`;\r\n }\r\n\r\n return `Chord ${normalizedChordKey}`;\r\n}\r\n\r\nexport {\r\n createChordDatabaseSearchData,\r\n createChordDegreeHints,\r\n createChordTransposition,\r\n describeChordKey,\r\n getAdjacentChromaticKey,\r\n getKeyDiatonicTriads,\r\n normalizeChordText,\r\n normalizeStructureKeyOption,\r\n parseMainKey,\r\n simplifyChordSymbol,\r\n transposeChordSymbol,\r\n transposePitchClass,\r\n intervalSemitones,\r\n};\r\n","import grandPianoUrl from './assets/webaudiofontdata/sound/0000_FluidR3_GM_sf2_file.js?url';\r\nimport electricPianoUrl from './assets/webaudiofontdata/sound/0040_FluidR3_GM_sf2_file.js?url';\r\nimport organUrl from './assets/webaudiofontdata/sound/0170_FluidR3_GM_sf2_file.js?url';\r\nimport nylonGuitarUrl from './assets/webaudiofontdata/sound/0240_FluidR3_GM_sf2_file.js?url';\r\nimport steelGuitarUrl from './assets/webaudiofontdata/sound/0250_FluidR3_GM_sf2_file.js?url';\r\nimport electricGuitarUrl from './assets/webaudiofontdata/sound/0270_FluidR3_GM_sf2_file.js?url';\r\nimport acousticBassUrl from './assets/webaudiofontdata/sound/0320_FluidR3_GM_sf2_file.js?url';\r\nimport electricBassUrl from './assets/webaudiofontdata/sound/0340_FluidR3_GM_sf2_file.js?url';\r\nimport { parseMainKey } from '../OpenLyric/domain/music-theory.ts';\r\nimport {\r\n getChordCardMidi,\r\n resolveChordCard,\r\n type ChordCard,\r\n type ChordDatabases,\r\n} from './chord-library.ts';\r\n\r\n/**\r\n * The strum player's ten instruments: the WebAudioFont soundfont each sounds\r\n * with, and how it voices a chord (`chord-library.ts`). How each is played,\r\n * and the tone the voice engine gives it, is in `instrument-sounds.ts`.\r\n *\r\n * - **Soundfonts** are General MIDI presets of the FluidR3_GM bank (MIT), as\r\n * published in `surikov.github.io/webaudiofontdata` (MIT), picked for a\r\n * worship band: steel-string, nylon and clean electric guitars, grand and\r\n * electric pianos, an organ, nylon guitar again for both ukuleles (General\r\n * MIDI has no ukulele), and an electric and an upright bass. FluidR3 was\r\n * chosen for realism: 22–44.1 kHz samples a few semitones apart, each\r\n * ringing its natural decay for seconds before any loop, and every zone in\r\n * tune within a few cents. The bass is an electric one because the upright\r\n * bass's samples sound almost nothing above 150 Hz: on small speakers its\r\n * line all but disappears. This app serves copies from\r\n * `assets/webaudiofontdata/sound/`, each pinned by `sha384-` hash and\r\n * checked at PACK time against the asset the build emitted, so a copy that\r\n * no longer matches fails the build instead of silently sounding different.\r\n * - **Voicings.** Every instrument voices the song key's chord itself, in\r\n * standard tuning, as its family does (`getVoicingInstrument`): the guitars\r\n * as a guitar, the keyboards as a piano, the basses as a bass. A pattern\r\n * strums that voicing.\r\n */\r\n\r\nexport type StrumInstrumentId =\r\n | 'guitar'\r\n | 'classical-guitar'\r\n | 'electric-guitar'\r\n | 'piano'\r\n | 'electric-piano'\r\n | 'organ'\r\n | 'ukulele'\r\n | 'baritone-ukulele'\r\n | 'bass'\r\n | 'upright-bass';\r\n\r\nexport interface StrumSoundfont {\r\n /** WebAudioFont data file name, e.g. `0250_FluidR3_GM_sf2_file`. */\r\n key: string;\r\n /** The global the data file assigns its preset to. */\r\n variable: string;\r\n /** Where this app serves its copy of the data file. */\r\n url: string;\r\n /**\r\n * `sha384-` hash of the published file, which the copy must match. Checked\r\n * at pack time against the emitted asset (`scripts/pack-packages.ts`), never\r\n * sent as an `integrity` attribute — see `webaudiofont.ts#loadScriptOnce`.\r\n */\r\n integrity: string;\r\n /** General MIDI program and sample bank, for display. */\r\n title: string;\r\n}\r\n\r\nexport interface StrumInstrument {\r\n id: StrumInstrumentId;\r\n label: string;\r\n soundfont: StrumSoundfont;\r\n}\r\n\r\nexport interface StrumVoicing {\r\n /** The key chord that is strummed, e.g. `E` or `C#m`. */\r\n chord: string;\r\n /** MIDI notes in string order (piano: low to high). */\r\n midi: number[];\r\n card: ChordCard;\r\n}\r\n\r\nfunction createSoundfont(\r\n key: string,\r\n title: string,\r\n url: string,\r\n integrity: string,\r\n): StrumSoundfont {\r\n return {\r\n key,\r\n variable: `_tone_${key}`,\r\n url,\r\n integrity,\r\n title,\r\n };\r\n}\r\n\r\nexport const STRUM_SOUNDFONTS = {\r\n steelGuitar: createSoundfont(\r\n '0250_FluidR3_GM_sf2_file',\r\n 'Acoustic Guitar (steel) · FluidR3 bank',\r\n steelGuitarUrl,\r\n 'sha384-JatEI/RMq9LuSL3MPVfxTf3eEwb4NTo0XfOm+H03F8NSxmpchEqNhhikdZqCEGJf',\r\n ),\r\n nylonGuitar: createSoundfont(\r\n '0240_FluidR3_GM_sf2_file',\r\n 'Acoustic Guitar (nylon) · FluidR3 bank',\r\n nylonGuitarUrl,\r\n 'sha384-pB9mj9I09XSMzqCZdNJgeTahU6z6qgIn0Q9gblWMnFhoWxWukgweEiXb5Esf8LyS',\r\n ),\r\n grandPiano: createSoundfont(\r\n '0000_FluidR3_GM_sf2_file',\r\n 'Acoustic Grand Piano · FluidR3 bank',\r\n grandPianoUrl,\r\n 'sha384-Opc1T4lx0TrCrkBPXj88YqbhYJHFpW35+rdWbxLX4xvc04bqnLgRZRpM9mgb4lGS',\r\n ),\r\n electricGuitar: createSoundfont(\r\n '0270_FluidR3_GM_sf2_file',\r\n 'Electric Guitar (clean) · FluidR3 bank',\r\n electricGuitarUrl,\r\n 'sha384-6MXNVE0idSDGHOzxi1U1K27VLNXwh31E8o2EbOMbi9oKUBARlpkilvCU+dkho31k',\r\n ),\r\n electricPiano: createSoundfont(\r\n '0040_FluidR3_GM_sf2_file',\r\n 'Electric Piano · FluidR3 bank',\r\n electricPianoUrl,\r\n 'sha384-rPje2Qq/hTvcSj+/PB8SCpzVJ+oNjIiZEKCPtjVVCEhjfLkfCGr7297Lm4GPB63i',\r\n ),\r\n organ: createSoundfont(\r\n '0170_FluidR3_GM_sf2_file',\r\n 'Percussive Organ · FluidR3 bank',\r\n organUrl,\r\n 'sha384-kijXLjp46P1NvQdW5OxeK1FYG4BZw7Th/ooYFGe3FzKDUPI1zBggVjBtbIvMMiyN',\r\n ),\r\n acousticBass: createSoundfont(\r\n '0320_FluidR3_GM_sf2_file',\r\n 'Acoustic Bass · FluidR3 bank',\r\n acousticBassUrl,\r\n 'sha384-ASEV4mQ6SA2eqvc6C0LEBfUbtP9ou7oten1II4ZoixNqwVfC+ONppDDykMphvNx+',\r\n ),\r\n electricBass: createSoundfont(\r\n '0340_FluidR3_GM_sf2_file',\r\n 'Electric Bass (pick) · FluidR3 bank',\r\n electricBassUrl,\r\n 'sha384-6bReDDQWbusyjn9xmyV23fkyimxADiQ6RkjfETqL6ZL57mJNsImJJm+4XLfP1WVy',\r\n ),\r\n} as const;\r\n\r\nexport const STRUM_INSTRUMENTS: readonly StrumInstrument[] = [\r\n {\r\n id: 'guitar',\r\n label: 'Acoustic Guitar',\r\n soundfont: STRUM_SOUNDFONTS.steelGuitar,\r\n },\r\n {\r\n id: 'classical-guitar',\r\n label: 'Classical Guitar',\r\n soundfont: STRUM_SOUNDFONTS.nylonGuitar,\r\n },\r\n {\r\n id: 'electric-guitar',\r\n label: 'Electric Guitar',\r\n soundfont: STRUM_SOUNDFONTS.electricGuitar,\r\n },\r\n { id: 'piano', label: 'Piano', soundfont: STRUM_SOUNDFONTS.grandPiano },\r\n {\r\n id: 'electric-piano',\r\n label: 'Electric Piano',\r\n soundfont: STRUM_SOUNDFONTS.electricPiano,\r\n },\r\n { id: 'organ', label: 'Organ', soundfont: STRUM_SOUNDFONTS.organ },\r\n {\r\n id: 'ukulele',\r\n label: 'Ukulele (GCEA)',\r\n soundfont: STRUM_SOUNDFONTS.nylonGuitar,\r\n },\r\n {\r\n id: 'baritone-ukulele',\r\n label: 'Baritone Ukulele (DGBE)',\r\n soundfont: STRUM_SOUNDFONTS.nylonGuitar,\r\n },\r\n {\r\n id: 'bass',\r\n label: 'Bass (EADG)',\r\n soundfont: STRUM_SOUNDFONTS.electricBass,\r\n },\r\n {\r\n id: 'upright-bass',\r\n label: 'Upright Bass (EADG)',\r\n soundfont: STRUM_SOUNDFONTS.acousticBass,\r\n },\r\n];\r\n\r\nconst SHARP_NAMES = [\r\n 'C',\r\n 'C#',\r\n 'D',\r\n 'D#',\r\n 'E',\r\n 'F',\r\n 'F#',\r\n 'G',\r\n 'G#',\r\n 'A',\r\n 'A#',\r\n 'B',\r\n];\r\nconst FLAT_NAMES = [\r\n 'C',\r\n 'Db',\r\n 'D',\r\n 'Eb',\r\n 'E',\r\n 'F',\r\n 'Gb',\r\n 'G',\r\n 'Ab',\r\n 'A',\r\n 'Bb',\r\n 'B',\r\n];\r\n\r\nexport function getStrumInstrument(id: string): StrumInstrument | null {\r\n return STRUM_INSTRUMENTS.find((instrument) => instrument.id === id) ?? null;\r\n}\r\n\r\n/**\r\n * The chord a pattern strums on `instrumentId` in `keyText` — the key's\r\n * tonic chord (`E`, `C#m`) — or `null` when the key is unreadable or the\r\n * instrument has no voicing for it.\r\n */\r\nexport function resolveStrumVoicing(\r\n instrumentId: StrumInstrumentId,\r\n keyText: string,\r\n databases: ChordDatabases,\r\n): StrumVoicing | null {\r\n const key = parseMainKey(keyText);\r\n\r\n if (!key) {\r\n return null;\r\n }\r\n\r\n const card = resolveChordCard(instrumentId, key.raw, databases);\r\n const midi = getChordCardMidi(card);\r\n\r\n return midi.length ? { chord: key.raw, midi, card } : null;\r\n}\r\n\r\n/** `56` → `G#3`, or `Ab3` in a key written with flats. */\r\nexport function getMidiNoteName(midi: number, keyText = ''): string {\r\n const names =\r\n (parseMainKey(keyText)?.alteration ?? 0) < 0 ? FLAT_NAMES : SHARP_NAMES;\r\n\r\n return `${names[((midi % 12) + 12) % 12]}${Math.floor(midi / 12) - 1}`;\r\n}\r\n","import {\r\n parseStrummingPatterns,\r\n parseStrummingPatternSteps,\r\n} from '../OpenLyric/preview/helpers.ts';\r\nimport { isValidStrummingPatternValue } from '../../editor/scripts/shared.ts';\r\n\r\n/**\r\n * Strum patterns in Open Lyric notation — the `Strumming Patterns` Config\r\n * field (`d--u-- | d-u---`), parsed by the notation's own parser\r\n * (`preview/helpers.ts`). One character is one sixteenth-note step; the\r\n * notation carries direction only, so each\r\n * stroke's loudness comes from where it falls in the bar\r\n * ({@link getStepVelocity}).\r\n */\r\n\r\nexport type StrumDirection = 'down' | 'up';\r\n\r\nexport interface StrumStep {\r\n direction: StrumDirection;\r\n /** Loudness, 0–1. */\r\n velocity: number;\r\n}\r\n\r\n/** What a page keeps and persists: the notation text is the source of truth. */\r\nexport interface StrumPatternDefinition {\r\n id: string;\r\n label: string;\r\n source: string;\r\n}\r\n\r\nexport interface StrumPattern extends StrumPatternDefinition {\r\n /** Step `i` is the `i`-th character of the grid; `null` = rest. */\r\n steps: (StrumStep | null)[];\r\n /** The count printed under each step. */\r\n counts: string[];\r\n /** 0-based steps a `|` or space separator precedes. */\r\n groupBreaks: number[];\r\n}\r\n\r\n/** Loudness by position: bar downbeat, other beats, off-beats, the rest. */\r\nexport const STEP_VELOCITIES = {\r\n downbeat: 0.9,\r\n beat: 0.75,\r\n offbeat: 0.65,\r\n subdivision: 0.55,\r\n} as const;\r\n\r\nconst EIGHTH_COUNTED_TIMES = new Set(['3/8', '6/8', '9/8', '12/8']);\r\nconst STRUMMING_PATTERNS_HEADER_RE = /^\\s*-?\\s*Strumming Patterns?\\s*:\\s*/i;\r\nconst OPEN_LYRIC_REST = '-';\r\n\r\nexport class StrumPatternError extends Error {}\r\n\r\nexport function getTimeSignatureParts(timeSignature: string): {\r\n beats: number;\r\n unit: number;\r\n} {\r\n const [beats, unit] = String(timeSignature ?? '')\r\n .split('/')\r\n .map((part) => Number.parseInt(part, 10));\r\n\r\n return {\r\n beats: Number.isFinite(beats) && beats > 0 ? beats : 4,\r\n unit: Number.isFinite(unit) && unit > 0 ? unit : 4,\r\n };\r\n}\r\n\r\nfunction isEighthCounted(timeSignature: string) {\r\n return EIGHTH_COUNTED_TIMES.has(String(timeSignature).trim());\r\n}\r\n\r\n/**\r\n * Seconds from one step to the next. The tempo counts the time signature's\r\n * unit, and a step is always a sixteenth note: `unit / 16` of a beat.\r\n */\r\nexport function getStrumStepSeconds(tempo: number, timeSignature: string) {\r\n const { unit } = getTimeSignatureParts(timeSignature);\r\n\r\n return (60 / tempo) * (unit / 16);\r\n}\r\n\r\n/**\r\n * The count under step `index`, as the notation's preview prints it\r\n * (`getStrummingStepLabel` in `preview/helpers.ts`): `1 e & a` per beat, or\r\n * `1 &` per eighth in the eighth-counted times.\r\n */\r\nexport function getOpenLyricStepCount(\r\n index: number,\r\n timeSignature: string,\r\n): string {\r\n if (isEighthCounted(timeSignature)) {\r\n return index % 2 === 0 ? String(Math.floor(index / 2) + 1) : '&';\r\n }\r\n\r\n const phase = index % 4;\r\n\r\n return phase === 0\r\n ? String(Math.floor(index / 4) + 1)\r\n : ['', 'e', '&', 'a'][phase];\r\n}\r\n\r\n/**\r\n * How loud a stroke on step `index` is. Sixteenth grid: the bar's first beat\r\n * is loudest, then the other beats, the `&`s, and the `e`/`a` sixteenths.\r\n * Eighth-counted times (steps counted `1 & 2 & …`, one number per eighth)\r\n * accent each dotted quarter — every third eighth, so every sixth step — then\r\n * the other eighths, and the `&` sixteenths between them the softest.\r\n */\r\nexport function getStepVelocity(index: number, timeSignature: string): number {\r\n const { beats } = getTimeSignatureParts(timeSignature);\r\n\r\n if (isEighthCounted(timeSignature)) {\r\n const barSteps = beats * 2;\r\n const inBar = index % barSteps;\r\n\r\n if (inBar === 0) {\r\n return STEP_VELOCITIES.downbeat;\r\n }\r\n\r\n if (inBar % 6 === 0) {\r\n return STEP_VELOCITIES.beat;\r\n }\r\n\r\n return inBar % 2 === 0\r\n ? STEP_VELOCITIES.offbeat\r\n : STEP_VELOCITIES.subdivision;\r\n }\r\n\r\n const inBar = index % (beats * 4);\r\n\r\n if (inBar === 0) {\r\n return STEP_VELOCITIES.downbeat;\r\n }\r\n\r\n switch (inBar % 4) {\r\n case 0:\r\n return STEP_VELOCITIES.beat;\r\n case 2:\r\n return STEP_VELOCITIES.offbeat;\r\n default:\r\n return STEP_VELOCITIES.subdivision;\r\n }\r\n}\r\n\r\n/** One Open Lyric pattern line, e.g. `d--u-- | d-u---`. */\r\nexport function parseOpenLyricPattern(\r\n definition: StrumPatternDefinition,\r\n timeSignature: string,\r\n): StrumPattern {\r\n if (!isValidStrummingPatternValue(definition.source)) {\r\n throw new StrumPatternError(\r\n `\"${definition.source}\" is not an Open Lyric strumming pattern. Use d/D/v/V (down), u/U/^ (up), x/X/1/*/+ (alternating), -/_/./0 (rest) and | or spaces between groups.`,\r\n );\r\n }\r\n\r\n const parsedSteps = parseStrummingPatternSteps(definition.source);\r\n\r\n return {\r\n ...definition,\r\n steps: parsedSteps.map((step, index): StrumStep | null =>\r\n step.direction === 'down' || step.direction === 'up'\r\n ? {\r\n direction: step.direction,\r\n velocity: getStepVelocity(index, timeSignature),\r\n }\r\n : null,\r\n ),\r\n counts: parsedSteps.map((_, index) =>\r\n getOpenLyricStepCount(index, timeSignature),\r\n ),\r\n groupBreaks: parsedSteps\r\n .filter((step) => step.breakBefore)\r\n .map((step) => step.index),\r\n };\r\n}\r\n\r\n/**\r\n * Patterns typed or pasted in: the body of a `Strumming Patterns` field,\r\n * `// label` lines included, its `- Strumming Patterns:` header optional.\r\n */\r\nexport function readStrumPatternInput(\r\n text: string,\r\n createId: () => string,\r\n nextLabel: () => string,\r\n): StrumPatternDefinition[] {\r\n const trimmed = String(text ?? '').trim();\r\n\r\n if (!trimmed) {\r\n throw new StrumPatternError('Type or paste a pattern first.');\r\n }\r\n\r\n const lines = trimmed\r\n .split(/\\r?\\n/)\r\n .map((line) => line.replace(STRUMMING_PATTERNS_HEADER_RE, ''));\r\n const patterns = parseStrummingPatterns(lines);\r\n\r\n if (!patterns.length) {\r\n throw new StrumPatternError('No pattern found — only comment lines.');\r\n }\r\n\r\n return patterns.map((pattern) => {\r\n const definition: StrumPatternDefinition = {\r\n id: createId(),\r\n label: pattern.labelSourceIndex == null ? nextLabel() : pattern.label,\r\n source: pattern.pattern,\r\n };\r\n\r\n parseOpenLyricPattern(definition, '4/4');\r\n return definition;\r\n });\r\n}\r\n\r\n/** The definition with step `index` cycled rest → down → up → rest. */\r\nexport function cycleStrumPatternStep(\r\n definition: StrumPatternDefinition,\r\n index: number,\r\n timeSignature: string,\r\n): StrumPatternDefinition {\r\n const pattern = parseOpenLyricPattern(definition, timeSignature);\r\n const parsedStep = parseStrummingPatternSteps(definition.source)[index];\r\n\r\n if (!parsedStep) {\r\n return definition;\r\n }\r\n\r\n const direction = pattern.steps[index]?.direction;\r\n const nextCharacter =\r\n direction === 'down' ? 'u' : direction === 'up' ? OPEN_LYRIC_REST : 'd';\r\n\r\n return {\r\n ...definition,\r\n source: `${definition.source.slice(0, parsedStep.sourceOffset)}${nextCharacter}${definition.source.slice(parsedStep.sourceOffset + 1)}`,\r\n };\r\n}\r\n","import type { StrumInstrumentId } from './instruments.ts';\r\n\r\n/**\r\n * How each of the strum player's instruments is played, and how it sounds —\r\n * the performance and tone the voice engine (`StrumSoundEngine.ts`) gives the\r\n * soundfont samples, so a pattern sounds like a player on a real instrument\r\n * rather than a sampler repeating itself:\r\n *\r\n * - **Technique.** The guitars and both ukuleles strum across their strings\r\n * in string order, so the re-entrant ukulele's high G sounds first on a\r\n * down stroke. A piano, electric piano or organ strikes the voicing as one\r\n * chord, the right hand alone on an up stroke. A bass plucks one note a\r\n * stroke.\r\n * - **Touch.** How fast a stroke crosses the strings, which strings an up\r\n * stroke catches, how much a stroke's force changes the sound\r\n * (`dynamics` — an organ's keys barely do), and how far a player's timing,\r\n * force and tuning wander from stroke to stroke (`humanize`).\r\n * - **Tone.** A soft stroke sounds darker than a hard one (`tone`), as on the\r\n * real instrument. The `body` filter takes a small instrument's low end away\r\n * — a ukulele sounds from nylon-guitar samples — and a bass's `drive` adds\r\n * the harmonics an amplifier gives it, which carry its line on small\r\n * speakers that cannot play its low fundamentals. The room is a short, warm\r\n * reverb.\r\n * - **Ring.** A string (a key) rings until it is struck again, damped, or\r\n * dies away. `sustain` shortens a small body's ring; `tail` fades a sample\r\n * once it runs into its loop (an organ holds its note); `damping` is how\r\n * fast a re-struck string lets go of its old note; `release` is a stop, the\r\n * player's palm (the piano's dampers) settling on the strings.\r\n *\r\n * Every time here is in seconds; a time constant is the time a sound takes to\r\n * fall to about a third (−8.7 dB).\r\n */\r\n\r\nexport type StrumTechnique = 'strum' | 'keys' | 'pluck';\r\n\r\nexport interface StrumSound {\r\n technique: StrumTechnique;\r\n /**\r\n * Seconds between neighbouring notes of a stroke at full force: a strum's\r\n * crossing of the strings, or how loosely a keyboard's chord lands.\r\n */\r\n noteGap: number;\r\n /** Share of the sounding strings an up stroke catches, from the highest. */\r\n upShare: number;\r\n /** Seconds between the notes of a chord card's roll. */\r\n rollGap: number;\r\n /** Low-pass cutoff (Hz), at middle C, of the softest and the hardest stroke. */\r\n tone: { soft: number; hard: number };\r\n /** How much a stroke's force changes its loudness: 1 fully, 0 not at all. */\r\n dynamics: number;\r\n /** Time constant of the body's decay on top of the sample's own; 0: none. */\r\n sustain: number;\r\n /** Time constant of the fade once a sample runs into its sustain loop. */\r\n tail: number;\r\n /** Longest a note rings. */\r\n maxRing: number;\r\n /** Time constant a re-struck string or key lets go of its old note with. */\r\n damping: number;\r\n /** Time constant of a stop: the palm or the dampers on the strings. */\r\n release: number;\r\n /** Pan of the outermost strings or keys, each side of centre (0–1). */\r\n width: number;\r\n /**\r\n * The body: a high-pass (Hz), an optional presence peak (Hz, dB), and an\r\n * optional `drive` — the share of amp-like harmonics mixed in (0–1).\r\n */\r\n body: {\r\n highpass: number;\r\n presence?: { frequency: number; gain: number };\r\n drive?: number;\r\n };\r\n /** Share of the sound sent to the room reverb (0–1). */\r\n room: number;\r\n /**\r\n * Output gain, balancing the instruments against each other: measured so a\r\n * pattern on each plays at about the same loudness (−13 LUFS).\r\n */\r\n level: number;\r\n /**\r\n * How far a player wanders, at most, each way: a stroke's timing\r\n * (seconds), a note's force (a share of it) and tuning (cents).\r\n */\r\n humanize: { timing: number; velocity: number; detune: number };\r\n}\r\n\r\nexport const STRUM_SOUNDS: Readonly<Record<StrumInstrumentId, StrumSound>> = {\r\n guitar: {\r\n technique: 'strum',\r\n noteGap: 0.0055,\r\n upShare: 0.6,\r\n rollGap: 0.045,\r\n tone: { soft: 4500, hard: 16000 },\r\n dynamics: 1,\r\n sustain: 0,\r\n tail: 0.6,\r\n maxRing: 7,\r\n damping: 0.012,\r\n release: 0.18,\r\n width: 0.25,\r\n body: { highpass: 70 },\r\n room: 0.2,\r\n level: 0.88,\r\n humanize: { timing: 0.004, velocity: 0.06, detune: 3 },\r\n },\r\n 'classical-guitar': {\r\n technique: 'strum',\r\n noteGap: 0.0065,\r\n upShare: 0.6,\r\n rollGap: 0.05,\r\n tone: { soft: 3500, hard: 11000 },\r\n dynamics: 1,\r\n sustain: 0,\r\n tail: 0.6,\r\n maxRing: 6,\r\n damping: 0.015,\r\n release: 0.2,\r\n width: 0.25,\r\n body: { highpass: 70 },\r\n room: 0.22,\r\n level: 0.88,\r\n humanize: { timing: 0.005, velocity: 0.07, detune: 3 },\r\n },\r\n 'electric-guitar': {\r\n technique: 'strum',\r\n noteGap: 0.005,\r\n upShare: 0.6,\r\n rollGap: 0.045,\r\n tone: { soft: 4000, hard: 12000 },\r\n dynamics: 1,\r\n sustain: 0,\r\n tail: 1,\r\n maxRing: 8,\r\n damping: 0.01,\r\n release: 0.15,\r\n width: 0.3,\r\n body: { highpass: 90 },\r\n room: 0.32,\r\n level: 0.76,\r\n humanize: { timing: 0.004, velocity: 0.06, detune: 2 },\r\n },\r\n piano: {\r\n technique: 'keys',\r\n noteGap: 0.003,\r\n upShare: 1,\r\n rollGap: 0.035,\r\n tone: { soft: 3000, hard: 14000 },\r\n dynamics: 1,\r\n sustain: 0,\r\n tail: 1.5,\r\n maxRing: 12,\r\n damping: 0.05,\r\n release: 0.25,\r\n width: 0.35,\r\n body: { highpass: 30 },\r\n room: 0.24,\r\n level: 1.22,\r\n humanize: { timing: 0.003, velocity: 0.05, detune: 1.5 },\r\n },\r\n 'electric-piano': {\r\n technique: 'keys',\r\n noteGap: 0.003,\r\n upShare: 1,\r\n rollGap: 0.035,\r\n tone: { soft: 2000, hard: 8000 },\r\n dynamics: 1,\r\n sustain: 3.5,\r\n tail: 1,\r\n maxRing: 10,\r\n damping: 0.05,\r\n release: 0.15,\r\n width: 0.45,\r\n body: { highpass: 40 },\r\n room: 0.22,\r\n level: 0.73,\r\n humanize: { timing: 0.003, velocity: 0.05, detune: 1.5 },\r\n },\r\n organ: {\r\n technique: 'keys',\r\n noteGap: 0.002,\r\n upShare: 1,\r\n rollGap: 0.035,\r\n tone: { soft: 12000, hard: 12000 },\r\n dynamics: 0.25,\r\n sustain: 0,\r\n tail: 60,\r\n maxRing: 4,\r\n damping: 0.02,\r\n release: 0.06,\r\n width: 0.3,\r\n body: { highpass: 40 },\r\n room: 0.28,\r\n level: 0.62,\r\n humanize: { timing: 0.003, velocity: 0, detune: 0 },\r\n },\r\n ukulele: {\r\n technique: 'strum',\r\n noteGap: 0.005,\r\n upShare: 1,\r\n rollGap: 0.04,\r\n tone: { soft: 5000, hard: 14000 },\r\n dynamics: 1,\r\n sustain: 2,\r\n tail: 0.6,\r\n maxRing: 3,\r\n damping: 0.012,\r\n release: 0.12,\r\n width: 0.15,\r\n body: { highpass: 200, presence: { frequency: 2200, gain: 2 } },\r\n room: 0.18,\r\n level: 0.97,\r\n humanize: { timing: 0.004, velocity: 0.07, detune: 4 },\r\n },\r\n 'baritone-ukulele': {\r\n technique: 'strum',\r\n noteGap: 0.0055,\r\n upShare: 0.75,\r\n rollGap: 0.045,\r\n tone: { soft: 3500, hard: 12000 },\r\n dynamics: 1,\r\n sustain: 1.4,\r\n tail: 0.45,\r\n maxRing: 4.5,\r\n damping: 0.012,\r\n release: 0.15,\r\n width: 0.2,\r\n body: { highpass: 110, presence: { frequency: 1800, gain: 1 } },\r\n room: 0.18,\r\n level: 1.11,\r\n humanize: { timing: 0.004, velocity: 0.06, detune: 3 },\r\n },\r\n bass: {\r\n technique: 'pluck',\r\n noteGap: 0,\r\n upShare: 1,\r\n rollGap: 0.18,\r\n tone: { soft: 2500, hard: 9000 },\r\n dynamics: 1,\r\n sustain: 2.2,\r\n tail: 0.6,\r\n maxRing: 5,\r\n damping: 0.02,\r\n release: 0.08,\r\n width: 0,\r\n body: { highpass: 35, drive: 0.45 },\r\n room: 0.05,\r\n level: 1.1,\r\n humanize: { timing: 0.004, velocity: 0.05, detune: 2 },\r\n },\r\n 'upright-bass': {\r\n technique: 'pluck',\r\n noteGap: 0,\r\n upShare: 1,\r\n rollGap: 0.2,\r\n tone: { soft: 1800, hard: 6000 },\r\n dynamics: 1,\r\n sustain: 1.4,\r\n tail: 0.5,\r\n maxRing: 4,\r\n damping: 0.03,\r\n release: 0.1,\r\n width: 0,\r\n body: { highpass: 35, drive: 0.7 },\r\n room: 0.08,\r\n level: 1.05,\r\n humanize: { timing: 0.004, velocity: 0.05, detune: 3 },\r\n },\r\n};\r\n\r\nexport function getStrumSound(instrument: StrumInstrumentId): StrumSound {\r\n return STRUM_SOUNDS[instrument];\r\n}\r\n","import type { StrumSound } from './instrument-sounds.ts';\r\nimport type { StrumDirection, StrumStep } from './patterns.ts';\r\n\r\n/**\r\n * How one step of a pattern becomes the notes a player sounds, and how a\r\n * chord card rolls its chord — the performance, before any sound is made\r\n * (`StrumSoundEngine.ts` plays it). Each instrument plays its own way\r\n * (`technique` in `instrument-sounds.ts`):\r\n *\r\n * - **Strum** (guitar, ukuleles). A down stroke crosses the strings in string\r\n * order, lowest string first; an up stroke comes back from the highest and\r\n * catches only the top `upShare` of them. A light down stroke — on an `e` or\r\n * `a` sixteenth — skips a guitar's lowest string. Each string sounds\r\n * `noteGap` after the one before: a little slower on a soft stroke, quicker\r\n * on an up stroke. The strings the pick meets last sound a little softer.\r\n * - **Keys** (piano). A down stroke strikes the whole voicing, an up stroke\r\n * the voicing without its lowest note — the right hand alone — a little\r\n * softer. The notes land within a few milliseconds of each other.\r\n * - **Pluck** (bass). A down stroke plucks the voicing's lowest note, its\r\n * root; an up stroke its highest, the octave. A bass sounds one note at a\r\n * time.\r\n *\r\n * A stroke's force is its step's velocity (`getStepVelocity`). A player is\r\n * human: each stroke lands up to `humanize.timing` off the grid, and each note\r\n * wanders up to `humanize.velocity` in force and `humanize.detune` in tuning.\r\n * `random` draws those; one that always returns `0.5` plays exactly.\r\n */\r\n\r\nexport interface PlannedNote {\r\n pitch: number;\r\n /** Seconds after the stroke lands — after the roll starts, for a roll. */\r\n offset: number;\r\n /** Loudness, 0–1. */\r\n velocity: number;\r\n /**\r\n * The string (a piano's key) that sounds it — its index in the voicing. A\r\n * new note on a string damps the one ringing there; a bass has one.\r\n */\r\n slot: number;\r\n /** Cents off pitch. */\r\n detune: number;\r\n /** Stereo position, −1 (left) to 1 (right). */\r\n pan: number;\r\n}\r\n\r\nexport interface StrokePlan {\r\n direction: StrumDirection;\r\n /** Seconds the stroke lands after its step; negative when early. */\r\n shift: number;\r\n /** The notes in the order the stroke meets them. */\r\n notes: PlannedNote[];\r\n}\r\n\r\n/** How loud a chord card's roll is. */\r\nexport const CHORD_ROLL_VELOCITY = 0.7;\r\n/** A down stroke this soft or softer skips the lowest string. */\r\nconst LIGHT_STROKE_VELOCITY = 0.6;\r\n/** The fewest strings a stroke skipping the lowest one leaves sounding. */\r\nconst LIGHT_STROKE_MIN_STRINGS = 4;\r\n/** An up stroke crosses the strings this much faster than a down stroke. */\r\nconst UP_STROKE_SPEED = 0.85;\r\n/** How much softer the last string a stroke meets is than the first. */\r\nconst DOWN_STROKE_TAPER = 0.12;\r\nconst UP_STROKE_TAPER = 0.25;\r\n/** A piano's right hand alone, on an up stroke. */\r\nconst RIGHT_HAND_FORCE = 0.9;\r\n/** How far each gap between strings, or roll notes, wanders (a share). */\r\nconst GAP_WANDER = 0.2;\r\n/** Where a piano's pan is centred, and the notes either side reaching full width. */\r\nconst PIANO_PAN_CENTRE = 60;\r\nconst PIANO_PAN_SPAN = 30;\r\nconst QUIETEST_NOTE = 0.05;\r\n\r\nfunction clamp(value: number, low: number, high: number) {\r\n return Math.min(high, Math.max(low, value));\r\n}\r\n\r\nfunction range(from: number, to: number) {\r\n return Array.from({ length: Math.max(0, to - from) }, (_, i) => from + i);\r\n}\r\n\r\n/** A draw between `-amount` and `amount`. */\r\nfunction wander(random: () => number, amount: number) {\r\n return (random() * 2 - 1) * amount;\r\n}\r\n\r\n/**\r\n * The voicing's notes a stroke sounds, as indexes into it, in the order the\r\n * stroke meets them.\r\n */\r\nexport function getStrokeStrings(\r\n count: number,\r\n direction: StrumDirection,\r\n velocity: number,\r\n sound: StrumSound,\r\n random: () => number,\r\n): number[] {\r\n if (!count) {\r\n return [];\r\n }\r\n\r\n switch (sound.technique) {\r\n case 'keys':\r\n return direction === 'up' && count >= 3\r\n ? range(1, count)\r\n : range(0, count);\r\n case 'pluck':\r\n return [direction === 'up' ? count - 1 : 0];\r\n default: {\r\n if (direction === 'down') {\r\n const skip =\r\n velocity <= LIGHT_STROKE_VELOCITY && count > LIGHT_STROKE_MIN_STRINGS\r\n ? 1\r\n : 0;\r\n\r\n return range(skip, count);\r\n }\r\n\r\n const caught = clamp(\r\n Math.round(count * sound.upShare + (random() - 0.5)),\r\n Math.min(2, count),\r\n count,\r\n );\r\n\r\n return range(count - caught, count).reverse();\r\n }\r\n }\r\n}\r\n\r\nfunction getPan(sound: StrumSound, slot: number, count: number, pitch: number) {\r\n switch (sound.technique) {\r\n case 'keys':\r\n return (\r\n clamp((pitch - PIANO_PAN_CENTRE) / PIANO_PAN_SPAN, -1, 1) * sound.width\r\n );\r\n case 'pluck':\r\n return 0;\r\n default:\r\n return count > 1 ? sound.width * ((2 * slot) / (count - 1) - 1) : 0;\r\n }\r\n}\r\n\r\n/** The notes of step `step`'s stroke, or `null` for a rest or no chord. */\r\nexport function planStroke(\r\n chord: readonly number[],\r\n step: StrumStep | null | undefined,\r\n sound: StrumSound,\r\n random: () => number = Math.random,\r\n): StrokePlan | null {\r\n if (!step || !chord.length) {\r\n return null;\r\n }\r\n\r\n const force = step.velocity;\r\n const up = step.direction === 'up';\r\n const strings = getStrokeStrings(\r\n chord.length,\r\n step.direction,\r\n force,\r\n sound,\r\n random,\r\n );\r\n const speed = (1.35 - 0.5 * force) * (up ? UP_STROKE_SPEED : 1);\r\n const taper = up ? UP_STROKE_TAPER : DOWN_STROKE_TAPER;\r\n const rightHand = up && strings.length < chord.length ? RIGHT_HAND_FORCE : 1;\r\n let offset = 0;\r\n\r\n const notes = strings.map((index, order): PlannedNote => {\r\n if (order > 0) {\r\n offset +=\r\n sound.technique === 'keys'\r\n ? 0\r\n : sound.noteGap * speed * (1 + wander(random, GAP_WANDER));\r\n }\r\n\r\n const pitch = chord[index];\r\n const reach = strings.length > 1 ? order / (strings.length - 1) : 0;\r\n let velocity: number;\r\n let noteOffset = offset;\r\n\r\n switch (sound.technique) {\r\n case 'keys':\r\n velocity = force * rightHand;\r\n noteOffset = order > 0 ? random() * 2 * sound.noteGap : 0;\r\n break;\r\n case 'pluck':\r\n velocity = force;\r\n break;\r\n default:\r\n velocity = force * (1 - taper * reach);\r\n }\r\n\r\n return {\r\n pitch,\r\n offset: noteOffset,\r\n velocity: clamp(\r\n velocity * (1 + wander(random, sound.humanize.velocity)),\r\n QUIETEST_NOTE,\r\n 1,\r\n ),\r\n slot: sound.technique === 'pluck' ? 0 : index,\r\n detune: wander(random, sound.humanize.detune),\r\n pan: getPan(sound, index, chord.length, pitch),\r\n };\r\n });\r\n\r\n return {\r\n direction: step.direction,\r\n shift: wander(random, sound.humanize.timing),\r\n notes,\r\n };\r\n}\r\n\r\n/**\r\n * A chord card's play: the notes one after another in the order given — from\r\n * the first string listed — `rollGap` apart, each on its own string.\r\n */\r\nexport function planChordRoll(\r\n midi: readonly number[],\r\n sound: StrumSound,\r\n random: () => number = Math.random,\r\n): PlannedNote[] {\r\n let offset = 0;\r\n\r\n return midi.map((pitch, index) => {\r\n if (index > 0) {\r\n offset += sound.rollGap * (1 + wander(random, GAP_WANDER));\r\n }\r\n\r\n return {\r\n pitch,\r\n offset,\r\n velocity: clamp(\r\n CHORD_ROLL_VELOCITY * (1 + wander(random, sound.humanize.velocity)),\r\n QUIETEST_NOTE,\r\n 1,\r\n ),\r\n slot: index,\r\n detune: wander(random, sound.humanize.detune),\r\n pan: getPan(sound, index, midi.length, pitch),\r\n };\r\n });\r\n}\r\n\r\n/** Seconds from a roll's first note to its last, before any wandering. */\r\nexport function getChordRollSeconds(sound: StrumSound, noteCount: number) {\r\n return Math.max(0, noteCount - 1) * sound.rollGap;\r\n}\r\n","import type { StrumSound } from './instrument-sounds.ts';\r\nimport type { WebAudioFontPreset, WebAudioFontZone } from './webaudiofont.ts';\r\n\r\n/**\r\n * The strum player's voice engine: plays soundfont samples the way a real\r\n * instrument sounds them, on the audio clock. It is this repo's own MIT code;\r\n * WebAudioFont only loads and decodes the samples (`webaudiofont.ts`).\r\n *\r\n * Each note is its sample, pitched from the soundfont zone nearest its key,\r\n * through a chain of its own:\r\n *\r\n * - **Tone** — a low-pass that opens with force and pitch, since a soft stroke\r\n * is darker than a hard one and a high string brighter than a low one.\r\n * - **Envelope** — the sample's own attack and decay. A body's `sustain`\r\n * shortens the ring, the sample's loop fades out over `tail` instead of\r\n * droning, and no note outlasts `maxRing`. Each zone's level is evened out\r\n * against the others, so no string of a chord jumps out, and each sample\r\n * starts where it becomes audible — some carry milliseconds of silence\r\n * before the pluck, which would make every note late.\r\n * - **Damper** — a string (`slot`) sounds one note: a new note there lets go\r\n * of the old one over `damping`, while the strings it does not touch ring\r\n * on. `damp` settles a whole group, as a palm on the strings.\r\n * - **Pan** — each string or key has its place across the stereo field.\r\n *\r\n * The notes of one instrument meet on its bus: the body (a high-pass, a\r\n * presence peak for a small instrument, and a bass's drive — soft saturation\r\n * band-limited to the harmonics a small speaker can play, mixed in under the\r\n * dry sound as an amplifier's would be), then the dry mix and a send into the\r\n * room — a convolution reverb whose impulse is synthesised here (a warm,\r\n * 1.3 s room), so no file is loaded for it. A peak limiter keeps loud strums\r\n * from clipping without flattening them.\r\n */\r\n\r\nexport interface StrumNote {\r\n preset: WebAudioFontPreset;\r\n sound: StrumSound;\r\n /** MIDI note. */\r\n pitch: number;\r\n /** Audio-clock time it sounds. */\r\n time: number;\r\n /** Loudness, 0–1. */\r\n velocity: number;\r\n /** Cents off pitch. */\r\n detune?: number;\r\n /** −1 (left) to 1 (right). */\r\n pan?: number;\r\n /** The string or key it sounds on: a new note there damps this one. */\r\n slot: string;\r\n /** The notes `damp` settles together, e.g. a pattern's or a chord card's. */\r\n group: string;\r\n}\r\n\r\nexport interface StrumVoice {\r\n /** Audio-clock time the note sounds. */\r\n readonly time: number;\r\n /** Silences the note if it has not sounded yet; a sounding note rings on. */\r\n cancel(): void;\r\n}\r\n\r\nexport interface StrumSoundEngine {\r\n /** Schedules a note; `null` when the soundfont has no sample for it. */\r\n play(note: StrumNote): StrumVoice | null;\r\n /**\r\n * Lets go of every note of `group` from `time`, over `timeConstant`. Notes\r\n * that would only sound after `time` are cancelled.\r\n */\r\n damp(group: string, time: number, timeConstant: number): void;\r\n}\r\n\r\n/** How a zone's sample is played: its level correction and where it starts. */\r\nexport interface ZoneTrim {\r\n /** Evens the zone's level out against the preset's other zones. */\r\n gain: number;\r\n /** Seconds into the sample it becomes audible. */\r\n start: number;\r\n}\r\n\r\n/** A note's fade-in, over the sample's own attack, so no start clicks. */\r\nconst ATTACK_SECONDS = 0.002;\r\n/** The last fade, before a sample without a loop runs out or `maxRing`. */\r\nconst END_FADE_SECONDS = 0.08;\r\n/** After this many time constants a damped note is silent (−60 dB). */\r\nconst TIME_CONSTANTS_TO_SILENCE = 7;\r\n/** How much a note's gain follows its force: `velocity ** 1.5`. */\r\nconst VELOCITY_CURVE = 1.5;\r\n/** A tone filter one octave up the keyboard opens this share of an octave. */\r\nconst TONE_KEY_TRACKING = 0.5;\r\nconst TONE_MAX_HZ = 18000;\r\n/** Resonance (dB) of a flat, Butterworth-like filter in the Web Audio API. */\r\nconst FLAT_Q_DB = -3;\r\n/** How far a zone's level may be evened out, either way. */\r\nconst MAX_ZONE_CORRECTION = 2;\r\n/** The stretch of each sample, after its peak, whose level is compared. */\r\nconst ZONE_LEVEL_SECONDS = 0.3;\r\n/** Where a sample's peak is looked for: its attack. */\r\nconst ZONE_PEAK_SECONDS = 0.5;\r\n/** A sample becomes audible where it first reaches this share of its peak (−40 dB). */\r\nconst AUDIBLE_SHARE = 0.01;\r\n/** How much of the silence before that a note keeps, so its onset stays whole. */\r\nconst ONSET_MARGIN_SECONDS = 0.0005;\r\n/** WebAudioFont's rate for a zone that names none. */\r\nconst DEFAULT_SAMPLE_RATE = 44100;\r\n/** The band a bass's drive adds its harmonics in (Hz). */\r\nconst DRIVE_BAND = { low: 120, high: 1600 } as const;\r\n/** How hard the drive saturates. */\r\nconst DRIVE_GAIN = 3;\r\n/** Its lean to one side, for the even harmonics an amplifier adds. */\r\nconst DRIVE_ASYMMETRY = 0.2;\r\n\r\nexport const ROOM = {\r\n /** Seconds the room takes to fall 60 dB. */\r\n decay: 1.3,\r\n /** Seconds before the first reflection. */\r\n preDelay: 0.012,\r\n /** How much darker the tail grows than the first reflections (0–1). */\r\n damping: 0.85,\r\n} as const;\r\n\r\nconst MASTER_GAIN = 0.7;\r\n/**\r\n * A peak limiter, not a compressor: it touches only the loudest strum attacks\r\n * (the top 4 dB), so a pick's transient keeps its bite. Its automatic makeup\r\n * gain lifts the whole mix about 2 dB.\r\n */\r\nconst LIMITER = {\r\n threshold: -4,\r\n knee: 3,\r\n ratio: 12,\r\n attack: 0.002,\r\n release: 0.12,\r\n} as const;\r\n\r\n/** A zone's key: its root, after the soundfont's tuning (semitones). */\r\nexport function getZoneRoot(zone: WebAudioFontZone): number {\r\n return (\r\n (zone.originalPitch ?? 6000) / 100 -\r\n (zone.coarseTune ?? 0) -\r\n (zone.fineTune ?? 0) / 100\r\n );\r\n}\r\n\r\n/**\r\n * The zone that sounds `pitch`: of those whose key range holds it, the one\r\n * whose root is nearest; else the zone whose range is nearest.\r\n */\r\nexport function findZone(\r\n preset: WebAudioFontPreset,\r\n pitch: number,\r\n): WebAudioFontZone | null {\r\n let best: WebAudioFontZone | null = null;\r\n let bestDistance = Infinity;\r\n\r\n for (const zone of preset.zones) {\r\n const low = zone.keyRangeLow ?? 0;\r\n const high = zone.keyRangeHigh ?? 127;\r\n const outside = pitch < low ? low - pitch : pitch > high ? pitch - high : 0;\r\n // A zone that holds the pitch always beats one that does not.\r\n const distance = outside * 1000 + Math.abs(pitch - getZoneRoot(zone));\r\n\r\n if (distance < bestDistance) {\r\n best = zone;\r\n bestDistance = distance;\r\n }\r\n }\r\n\r\n return best;\r\n}\r\n\r\n/** Whether the zone has a sustain loop (WebAudioFont's rule). */\r\nexport function hasLoop(zone: WebAudioFontZone): boolean {\r\n return (\r\n (zone.loopStart ?? 0) >= 1 && (zone.loopEnd ?? 0) > (zone.loopStart ?? 0)\r\n );\r\n}\r\n\r\n/** Low-pass cutoff (Hz) for a note of `velocity` at `pitch`. */\r\nexport function getToneCutoff(\r\n sound: StrumSound,\r\n velocity: number,\r\n pitch: number,\r\n): number {\r\n const force = Math.min(1, Math.max(0, (velocity - 0.4) / 0.6));\r\n const cutoff =\r\n sound.tone.soft *\r\n (sound.tone.hard / sound.tone.soft) ** force *\r\n 2 ** ((TONE_KEY_TRACKING * (pitch - 60)) / 12);\r\n\r\n return Math.min(TONE_MAX_HZ, Math.max(sound.tone.soft / 2, cutoff));\r\n}\r\n\r\n/**\r\n * The gain a note of `velocity` sounds at, before its zone and instrument:\r\n * `velocity ** 1.5`, as far as the instrument's `dynamics` lets force count.\r\n */\r\nexport function getVelocityGain(velocity: number, dynamics = 1): number {\r\n const force = Math.max(0, Math.min(1, velocity)) ** VELOCITY_CURVE;\r\n\r\n return 1 - dynamics + dynamics * force;\r\n}\r\n\r\n/**\r\n * The drive's transfer curve: a soft saturation leaning to one side, so it\r\n * adds both odd and even harmonics, from −1 to 1 over `size` points.\r\n */\r\nexport function createDriveCurve(size = 1024): Float32Array<ArrayBuffer> {\r\n const curve = new Float32Array(size);\r\n\r\n for (let i = 0; i < size; i += 1) {\r\n const x = (i / (size - 1)) * 2 - 1;\r\n\r\n curve[i] = Math.tanh(DRIVE_GAIN * x) + DRIVE_ASYMMETRY * x * x;\r\n }\r\n\r\n return curve;\r\n}\r\n\r\n/**\r\n * A sample's level — its RMS over {@link ZONE_LEVEL_SECONDS} from its peak —\r\n * and where it becomes audible, just before it first reaches −40 dB.\r\n */\r\nfunction measureSample(buffer: AudioBuffer): { level: number; start: number } {\r\n const data = buffer.getChannelData(0);\r\n const rate = buffer.sampleRate;\r\n const attackEnd = Math.min(data.length, Math.round(ZONE_PEAK_SECONDS * rate));\r\n let peak = 0;\r\n let peakIndex = 0;\r\n\r\n for (let i = 0; i < attackEnd; i += 1) {\r\n const value = Math.abs(data[i]);\r\n\r\n if (value > peak) {\r\n peak = value;\r\n peakIndex = i;\r\n }\r\n }\r\n\r\n let audible = 0;\r\n\r\n while (\r\n audible < peakIndex &&\r\n Math.abs(data[audible]) < peak * AUDIBLE_SHARE\r\n ) {\r\n audible += 1;\r\n }\r\n\r\n const end = Math.min(\r\n data.length,\r\n peakIndex + Math.round(ZONE_LEVEL_SECONDS * rate),\r\n );\r\n let sum = 0;\r\n\r\n for (let i = peakIndex; i < end; i += 1) {\r\n sum += data[i] * data[i];\r\n }\r\n\r\n return {\r\n level: Math.sqrt(sum / Math.max(1, end - peakIndex)),\r\n start: Math.max(0, audible / rate - ONSET_MARGIN_SECONDS),\r\n };\r\n}\r\n\r\nconst zoneTrims = new WeakMap<\r\n WebAudioFontPreset,\r\n Map<WebAudioFontZone, ZoneTrim>\r\n>();\r\n\r\n/**\r\n * How to play a zone's sample, measured once per preset: the gain that evens\r\n * its level out — the preset's median level over the zone's, within ×½ to\r\n * ×2 — and the silence to skip before it.\r\n */\r\nexport function getZoneTrim(\r\n preset: WebAudioFontPreset,\r\n zone: WebAudioFontZone,\r\n): ZoneTrim {\r\n let trims = zoneTrims.get(preset);\r\n\r\n if (!trims) {\r\n const measured = new Map<\r\n WebAudioFontZone,\r\n { level: number; start: number }\r\n >();\r\n\r\n for (const each of preset.zones) {\r\n if (each.buffer) {\r\n measured.set(each, measureSample(each.buffer));\r\n }\r\n }\r\n\r\n const levels = [...measured.values()]\r\n .map(({ level }) => level)\r\n .filter((level) => level > 0)\r\n .sort((a, b) => a - b);\r\n const median = levels[Math.floor(levels.length / 2)] ?? 0;\r\n\r\n trims = new Map();\r\n\r\n for (const [each, { level, start }] of measured) {\r\n trims.set(each, {\r\n gain:\r\n level > 0 && median > 0\r\n ? Math.min(\r\n MAX_ZONE_CORRECTION,\r\n Math.max(1 / MAX_ZONE_CORRECTION, median / level),\r\n )\r\n : 1,\r\n start,\r\n });\r\n }\r\n\r\n zoneTrims.set(preset, trims);\r\n }\r\n\r\n return trims.get(zone) ?? { gain: 1, start: 0 };\r\n}\r\n\r\n/**\r\n * A stereo room impulse: noise decaying `decay` seconds to −60 dB after a\r\n * short pre-delay, darkening as it fades, each channel drawn apart so the room\r\n * sounds wide.\r\n */\r\nexport function createRoomImpulse(\r\n sampleRate: number,\r\n random: () => number = Math.random,\r\n { decay, preDelay, damping }: typeof ROOM = ROOM,\r\n): [Float32Array, Float32Array] {\r\n const length = Math.round((preDelay + decay) * sampleRate);\r\n const delay = Math.round(preDelay * sampleRate);\r\n const channels: [Float32Array, Float32Array] = [\r\n new Float32Array(length),\r\n new Float32Array(length),\r\n ];\r\n\r\n for (const data of channels) {\r\n let low = 0;\r\n\r\n for (let i = delay; i < length; i += 1) {\r\n const t = (i - delay) / sampleRate;\r\n // −60 dB at `decay`: e^(−6.9·t/decay).\r\n const level = Math.exp((-6.9 * t) / decay);\r\n // A one-pole low-pass closing as the tail fades: air and walls take the\r\n // highs first.\r\n const smoothing = damping * Math.min(1, t / decay) ** 0.5;\r\n\r\n low = low * smoothing + (random() * 2 - 1) * (1 - smoothing);\r\n data[i] = low * level;\r\n }\r\n }\r\n\r\n return channels;\r\n}\r\n\r\ninterface Bus {\r\n input: GainNode;\r\n}\r\n\r\ninterface Voice extends StrumVoice {\r\n slot: string;\r\n group: string;\r\n /** When the note stops sounding (scheduled). */\r\n end: number;\r\n source: AudioBufferSourceNode;\r\n damper: GainNode;\r\n /** Settled already: by a damp, a cancel, or its end. */\r\n done: boolean;\r\n}\r\n\r\nexport class WebAudioStrumSoundEngine implements StrumSoundEngine {\r\n #context: BaseAudioContext;\r\n #master: GainNode;\r\n #room: AudioNode;\r\n #buses = new Map<StrumSound, Bus>();\r\n #slots = new Map<string, Voice>();\r\n #groups = new Map<string, Set<Voice>>();\r\n\r\n constructor(\r\n context: BaseAudioContext,\r\n destination: AudioNode = context.destination,\r\n ) {\r\n this.#context = context;\r\n\r\n const limiter = context.createDynamicsCompressor();\r\n\r\n limiter.threshold.value = LIMITER.threshold;\r\n limiter.knee.value = LIMITER.knee;\r\n limiter.ratio.value = LIMITER.ratio;\r\n limiter.attack.value = LIMITER.attack;\r\n limiter.release.value = LIMITER.release;\r\n limiter.connect(destination);\r\n\r\n this.#master = context.createGain();\r\n this.#master.gain.value = MASTER_GAIN;\r\n this.#master.connect(limiter);\r\n\r\n const convolver = context.createConvolver();\r\n const impulse = context.createBuffer(\r\n 2,\r\n Math.round((ROOM.preDelay + ROOM.decay) * context.sampleRate),\r\n context.sampleRate,\r\n );\r\n\r\n createRoomImpulse(context.sampleRate).forEach((data, channel) =>\r\n impulse.getChannelData(channel).set(data),\r\n );\r\n convolver.buffer = impulse;\r\n convolver.connect(this.#master);\r\n this.#room = convolver;\r\n }\r\n\r\n play(note: StrumNote): StrumVoice | null {\r\n const zone = findZone(note.preset, note.pitch);\r\n const buffer = zone?.buffer;\r\n\r\n if (!zone || !buffer) {\r\n return null;\r\n }\r\n\r\n const context = this.#context;\r\n const { sound } = note;\r\n const time = Math.max(note.time, context.currentTime);\r\n const rate =\r\n 2 ** ((note.pitch - getZoneRoot(zone) + (note.detune ?? 0) / 100) / 12);\r\n const source = context.createBufferSource();\r\n const loops = hasLoop(zone);\r\n const delay = zone.delay ?? 0;\r\n const trim = getZoneTrim(note.preset, zone);\r\n const offset = delay + trim.start;\r\n\r\n source.buffer = buffer;\r\n source.playbackRate.value = rate;\r\n\r\n if (loops) {\r\n const sampleRate = zone.sampleRate ?? DEFAULT_SAMPLE_RATE;\r\n\r\n source.loop = true;\r\n source.loopStart = zone.loopStart! / sampleRate + delay;\r\n source.loopEnd = zone.loopEnd! / sampleRate + delay;\r\n }\r\n\r\n // How long the sample plays, in real time, before its loop or its end.\r\n const natural =\r\n Math.max(0, (loops ? source.loopStart : buffer.duration) - offset) / rate;\r\n const ring = Math.max(\r\n ATTACK_SECONDS + END_FADE_SECONDS,\r\n loops ? sound.maxRing : Math.min(sound.maxRing, natural),\r\n );\r\n const end = time + ring;\r\n\r\n const tone = context.createBiquadFilter();\r\n\r\n tone.type = 'lowpass';\r\n tone.frequency.value = getToneCutoff(sound, note.velocity, note.pitch);\r\n tone.Q.value = FLAT_Q_DB;\r\n\r\n const envelope = context.createGain();\r\n const gain = envelope.gain;\r\n\r\n gain.setValueAtTime(0, time);\r\n gain.linearRampToValueAtTime(\r\n getVelocityGain(note.velocity, sound.dynamics) * trim.gain,\r\n time + ATTACK_SECONDS,\r\n );\r\n\r\n if (sound.sustain > 0) {\r\n gain.setTargetAtTime(0, time + ATTACK_SECONDS, sound.sustain);\r\n }\r\n\r\n if (loops && natural < ring) {\r\n gain.setTargetAtTime(\r\n 0,\r\n time + Math.max(ATTACK_SECONDS, natural),\r\n sound.tail,\r\n );\r\n }\r\n\r\n gain.setTargetAtTime(0, end - END_FADE_SECONDS, END_FADE_SECONDS / 5);\r\n\r\n const damper = context.createGain();\r\n const panner =\r\n note.pan && typeof context.createStereoPanner === 'function'\r\n ? context.createStereoPanner()\r\n : null;\r\n\r\n source.connect(tone);\r\n tone.connect(envelope);\r\n envelope.connect(damper);\r\n\r\n if (panner) {\r\n panner.pan.value = Math.max(-1, Math.min(1, note.pan!));\r\n damper.connect(panner);\r\n panner.connect(this.#getBus(sound).input);\r\n } else {\r\n damper.connect(this.#getBus(sound).input);\r\n }\r\n\r\n const voice: Voice = {\r\n time,\r\n end,\r\n slot: note.slot,\r\n group: note.group,\r\n source,\r\n damper,\r\n done: false,\r\n cancel: () => {\r\n if (!voice.done && voice.time > context.currentTime) {\r\n this.#stopVoice(voice, context.currentTime);\r\n }\r\n },\r\n };\r\n\r\n source.onended = () => {\r\n voice.done = true;\r\n this.#forget(voice);\r\n source.disconnect();\r\n damper.disconnect();\r\n panner?.disconnect();\r\n };\r\n source.start(time, offset);\r\n source.stop(end);\r\n\r\n const previous = this.#slots.get(note.slot);\r\n\r\n if (previous && !previous.done) {\r\n this.#dampVoice(previous, time, sound.damping);\r\n }\r\n\r\n this.#slots.set(note.slot, voice);\r\n\r\n let group = this.#groups.get(note.group);\r\n\r\n if (!group) {\r\n group = new Set();\r\n this.#groups.set(note.group, group);\r\n }\r\n\r\n group.add(voice);\r\n return voice;\r\n }\r\n\r\n damp(group: string, time: number, timeConstant: number): void {\r\n for (const voice of this.#groups.get(group) ?? []) {\r\n this.#dampVoice(voice, time, timeConstant);\r\n }\r\n }\r\n\r\n #dampVoice(voice: Voice, time: number, timeConstant: number) {\r\n if (voice.done) {\r\n return;\r\n }\r\n\r\n if (voice.time >= time) {\r\n this.#stopVoice(voice, this.#context.currentTime);\r\n return;\r\n }\r\n\r\n const stop = time + timeConstant * TIME_CONSTANTS_TO_SILENCE;\r\n\r\n voice.done = true;\r\n voice.damper.gain.setTargetAtTime(0, time, timeConstant);\r\n\r\n if (stop < voice.end) {\r\n voice.end = stop;\r\n stopSource(voice.source, stop);\r\n }\r\n }\r\n\r\n #stopVoice(voice: Voice, time: number) {\r\n voice.done = true;\r\n voice.end = time;\r\n stopSource(voice.source, time);\r\n this.#forget(voice);\r\n }\r\n\r\n #forget(voice: Voice) {\r\n if (this.#slots.get(voice.slot) === voice) {\r\n this.#slots.delete(voice.slot);\r\n }\r\n\r\n this.#groups.get(voice.group)?.delete(voice);\r\n }\r\n\r\n #getBus(sound: StrumSound): Bus {\r\n let bus = this.#buses.get(sound);\r\n\r\n if (!bus) {\r\n const context = this.#context;\r\n const input = context.createGain();\r\n const highpass = context.createBiquadFilter();\r\n const send = context.createGain();\r\n let output: AudioNode = highpass;\r\n\r\n input.gain.value = sound.level;\r\n highpass.type = 'highpass';\r\n highpass.frequency.value = sound.body.highpass;\r\n highpass.Q.value = FLAT_Q_DB;\r\n input.connect(highpass);\r\n\r\n if (sound.body.presence) {\r\n const presence = context.createBiquadFilter();\r\n\r\n presence.type = 'peaking';\r\n presence.frequency.value = sound.body.presence.frequency;\r\n presence.gain.value = sound.body.presence.gain;\r\n presence.Q.value = 1;\r\n highpass.connect(presence);\r\n output = presence;\r\n }\r\n\r\n if (sound.body.drive) {\r\n output = this.#addDrive(output, sound.body.drive);\r\n }\r\n\r\n send.gain.value = sound.room;\r\n output.connect(this.#master);\r\n output.connect(send);\r\n send.connect(this.#room);\r\n bus = { input };\r\n this.#buses.set(sound, bus);\r\n }\r\n\r\n return bus;\r\n }\r\n\r\n /**\r\n * Mixes a drive into `body`: the dry sound plus `amount` of its soft\r\n * saturation, kept to {@link DRIVE_BAND}. Returns the node carrying both.\r\n */\r\n #addDrive(body: AudioNode, amount: number): AudioNode {\r\n const context = this.#context;\r\n const sum = context.createGain();\r\n const shaper = context.createWaveShaper();\r\n const low = context.createBiquadFilter();\r\n const high = context.createBiquadFilter();\r\n const mix = context.createGain();\r\n\r\n shaper.curve = createDriveCurve();\r\n shaper.oversample = '2x';\r\n // The high-pass also takes away the curve's lean, a DC offset.\r\n low.type = 'highpass';\r\n low.frequency.value = DRIVE_BAND.low;\r\n low.Q.value = FLAT_Q_DB;\r\n high.type = 'lowpass';\r\n high.frequency.value = DRIVE_BAND.high;\r\n high.Q.value = FLAT_Q_DB;\r\n mix.gain.value = amount;\r\n body.connect(sum);\r\n body.connect(shaper);\r\n shaper.connect(low);\r\n low.connect(high);\r\n high.connect(mix);\r\n mix.connect(sum);\r\n return sum;\r\n }\r\n}\r\n\r\n/** `stop` again, to bring a scheduled end forward; old Safari throws on it. */\r\nfunction stopSource(source: AudioBufferSourceNode, time: number) {\r\n try {\r\n source.stop(time);\r\n } catch {\r\n // Already stopped: nothing left to silence.\r\n }\r\n}\r\n","import type { StrumSoundfont } from './instruments.ts';\r\nimport webAudioFontPlayerUrl from './assets/webaudiofont/WebAudioFontPlayer.js?url';\r\n\r\n/**\r\n * The strum player's soundfonts are loaded and decoded by Sergey Surikov's\r\n * WebAudioFont, at runtime, with a `<script>` per file from copies this app\r\n * serves itself (`assets/`, whose README says where each came from). No other\r\n * host is involved. The notes are then played by this repo's own voice engine\r\n * (`StrumSoundEngine.ts`), not by WebAudioFont's player:\r\n *\r\n * - The player is webaudiofont **2.5.57**, the npm package's\r\n * `npm/dist/WebAudioFontPlayer.js` byte for byte. It is GPL-3.0-or-later,\r\n * so it stays a file of its own, beside its license, rather than bundled\r\n * into this MIT code. Only its `adjustPreset` is called, to decode the\r\n * samples.\r\n * - The soundfonts are MIT files from `surikov.github.io/webaudiofontdata`\r\n * (see `STRUM_SOUNDFONTS`).\r\n *\r\n * Every file carries a `sha384-` pin, and it is checked **where a pin can be\r\n * trusted: at pack time**, by `scripts/pack-packages.ts`, against the asset the\r\n * build actually emitted. It is deliberately NOT sent as an `integrity`\r\n * attribute at runtime — see `loadScriptOnce`. Each preset is decoded\r\n * completely before it is handed back, so the first stroke of a fresh load is\r\n * never dropped.\r\n */\r\n\r\n/**\r\n * One sample of a soundfont and the keys it sounds. `adjustPreset` fills in\r\n * the numbers a data file leaves out, and decodes `buffer`.\r\n */\r\nexport interface WebAudioFontZone {\r\n buffer?: AudioBuffer;\r\n keyRangeLow?: number;\r\n keyRangeHigh?: number;\r\n /** The sample's pitch, in cents of a MIDI note (`6000` = middle C). */\r\n originalPitch?: number;\r\n /** Semitones and cents the zone retunes the sample by. */\r\n coarseTune?: number;\r\n fineTune?: number;\r\n /** The sample's own rate, which `loopStart` and `loopEnd` count in. */\r\n sampleRate?: number;\r\n loopStart?: number;\r\n loopEnd?: number;\r\n /** Seconds into the buffer the sample starts. */\r\n delay?: number;\r\n}\r\n\r\nexport interface WebAudioFontPreset {\r\n zones: WebAudioFontZone[];\r\n}\r\n\r\nexport interface WebAudioFontPlayerInstance {\r\n /** Decodes every zone's sample into its `buffer`, some asynchronously. */\r\n adjustPreset(context: BaseAudioContext, preset: WebAudioFontPreset): void;\r\n}\r\n\r\nexport type WebAudioFontPlayerConstructor =\r\n new () => WebAudioFontPlayerInstance;\r\n\r\n/**\r\n * `integrity` is the pin `pack-packages.ts` checks the emitted asset against.\r\n * Nothing reads it at runtime; it is data, and it must stay in the bundle for\r\n * that check to have something to check.\r\n */\r\nexport const WEBAUDIOFONT_PLAYER_SCRIPT = {\r\n url: webAudioFontPlayerUrl,\r\n integrity:\r\n 'sha384-Dvf7U4nlcDoz6ty0yygUFW1OkoynFUAy62/rMn8ERLrMEaW5Spe0Vqt2oq6gJw6U',\r\n} as const;\r\n\r\nconst DECODE_POLL_MS = 25;\r\nconst DECODE_TIMEOUT_MS = 30_000;\r\n\r\nconst scriptLoads = new Map<string, Promise<void>>();\r\n\r\n/**\r\n * Adds `<script src>` once per URL; a failed load may be retried.\r\n *\r\n * **No `integrity` / `crossOrigin` here, on purpose.** These URLs are not a\r\n * CDN's — they are whatever the host's bundler resolved `?url` to, served from\r\n * the host's own origin, out of files shipped inside this package. SRI guards\r\n * against a third party serving different bytes; there is no third party on\r\n * this path, and the attribute cannot survive one either:\r\n *\r\n * - a Vite dev host serves a `.js` file under `node_modules` through its\r\n * transform pipeline with an inline sourcemap appended (the player script:\r\n * 108,616 bytes on disk, 420,383 served), so the hash never matches and the\r\n * chord simply does not sound;\r\n * - `crossOrigin=\"anonymous\"` makes it a CORS request, which a custom\r\n * app protocol (`owa://…`) need not answer.\r\n *\r\n * The guarantee the pin is there for — a copy that drifted by a byte must not\r\n * ship — is kept, and kept *earlier*: `scripts/pack-packages.ts` hashes the\r\n * asset the build emitted and fails the pack on a mismatch, rather than\r\n * leaving it to fail on a projector in a church hall.\r\n */\r\nexport function loadScriptOnce(url: string): Promise<void> {\r\n const pending = scriptLoads.get(url);\r\n\r\n if (pending) {\r\n return pending;\r\n }\r\n\r\n const load = new Promise<void>((resolve, reject) => {\r\n const script = document.createElement('script');\r\n\r\n script.src = url;\r\n script.async = true;\r\n script.addEventListener('load', () => resolve(), { once: true });\r\n script.addEventListener(\r\n 'error',\r\n () => {\r\n scriptLoads.delete(url);\r\n script.remove();\r\n reject(new Error(`Could not load ${url} (not served, or blocked).`));\r\n },\r\n { once: true },\r\n );\r\n document.head.appendChild(script);\r\n });\r\n\r\n scriptLoads.set(url, load);\r\n return load;\r\n}\r\n\r\nexport async function loadWebAudioFontPlayer(): Promise<WebAudioFontPlayerConstructor> {\r\n await loadScriptOnce(WEBAUDIOFONT_PLAYER_SCRIPT.url);\r\n\r\n const Player = (globalThis as any).WebAudioFontPlayer;\r\n\r\n if (typeof Player !== 'function') {\r\n throw new Error('The WebAudioFont script loaded but defined no player.');\r\n }\r\n\r\n return Player;\r\n}\r\n\r\nfunction waitForDecodedZones(preset: WebAudioFontPreset): Promise<void> {\r\n const startedAt = Date.now();\r\n\r\n return new Promise((resolve, reject) => {\r\n const check = () => {\r\n if (preset.zones.every((zone) => zone.buffer)) {\r\n resolve();\r\n } else if (Date.now() - startedAt > DECODE_TIMEOUT_MS) {\r\n reject(new Error('Timed out decoding the soundfont samples.'));\r\n } else {\r\n setTimeout(check, DECODE_POLL_MS);\r\n }\r\n };\r\n\r\n check();\r\n });\r\n}\r\n\r\n/**\r\n * Loads a soundfont's data file and decodes every sample of it\r\n * (`player.adjustPreset`), then waits until each zone has its buffer.\r\n */\r\nexport async function loadSoundfontPreset(\r\n player: WebAudioFontPlayerInstance,\r\n context: BaseAudioContext,\r\n soundfont: StrumSoundfont,\r\n): Promise<WebAudioFontPreset> {\r\n await loadScriptOnce(soundfont.url);\r\n\r\n const preset = (globalThis as any)[soundfont.variable];\r\n\r\n if (!preset || !Array.isArray(preset.zones)) {\r\n throw new Error(`${soundfont.key} loaded but defined no preset.`);\r\n }\r\n\r\n player.adjustPreset(context, preset);\r\n await waitForDecodedZones(preset);\r\n return preset;\r\n}\r\n","export default \"__VITE_ASSET__6kssa3xCT2YlMpIbbRNP$Q__\"","import { getStrumSound, type StrumSound } from './instrument-sounds.ts';\r\nimport {\r\n getStrumInstrument,\r\n type StrumInstrument,\r\n type StrumInstrumentId,\r\n type StrumSoundfont,\r\n} from './instruments.ts';\r\nimport { getStrumStepSeconds, type StrumPattern } from './patterns.ts';\r\nimport {\r\n planChordRoll,\r\n planStroke,\r\n type PlannedNote,\r\n type StrokePlan,\r\n} from './strum-plan.ts';\r\nimport {\r\n WebAudioStrumSoundEngine,\r\n type StrumSoundEngine,\r\n type StrumVoice,\r\n} from './StrumSoundEngine.ts';\r\nimport {\r\n loadSoundfontPreset,\r\n loadWebAudioFontPlayer,\r\n type WebAudioFontPlayerConstructor,\r\n type WebAudioFontPlayerInstance,\r\n type WebAudioFontPreset,\r\n} from './webaudiofont.ts';\r\n\r\n/**\r\n * Plays one strum pattern at a time, looping, on the audio clock: step 1\r\n * sounds {@link START_DELAY_SECONDS} after the press, each later step one\r\n * step-length after the one before (`getStrumStepSeconds`), and after the last\r\n * step the grid starts again. A short timer queues strokes up to\r\n * {@link SCHEDULE_AHEAD_SECONDS} ahead. Each stroke is played the way its\r\n * instrument is (`planStroke`) and sounded by the voice engine\r\n * (`StrumSoundEngine.ts`), where every string rings until it is struck again.\r\n *\r\n * A stop cancels the queued strokes that have not sounded yet, lets a stroke\r\n * under way finish, and then damps the strings still ringing, as a player's\r\n * palm does (the instrument's `release`).\r\n *\r\n * It also plays a chord card's roll (`planChordRoll`), alongside a pattern and\r\n * untouched by a stop. A new roll damps the one before it.\r\n */\r\n\r\nexport interface StrumPlaybackRequest {\r\n pattern: StrumPattern;\r\n tempo: number;\r\n timeSignature: string;\r\n /** MIDI notes the pattern strums, in string order (piano: low to high). */\r\n chord: readonly number[];\r\n instrument: StrumInstrumentId;\r\n}\r\n\r\nexport interface StrumChordRequest {\r\n /** The chord's notes, in the order they are struck. */\r\n midi: readonly number[];\r\n instrument: StrumInstrumentId;\r\n}\r\n\r\nexport type StrumPlayerState = 'idle' | 'loading' | 'playing';\r\n\r\n/** Browser seams, swapped out by the unit tests. */\r\nexport interface StrumPlayerRuntime {\r\n createAudioContext(): AudioContext;\r\n loadPlayer(): Promise<WebAudioFontPlayerConstructor>;\r\n loadPreset(\r\n player: WebAudioFontPlayerInstance,\r\n context: BaseAudioContext,\r\n soundfont: StrumSoundfont,\r\n ): Promise<WebAudioFontPreset>;\r\n /** The voice engine that sounds the notes, on the player's context. */\r\n createEngine(context: AudioContext): StrumSoundEngine;\r\n /** Draws a player's human wander, `0 ≤ n < 1`. Default `Math.random`. */\r\n random?(): number;\r\n}\r\n\r\nexport const browserStrumPlayerRuntime: StrumPlayerRuntime = {\r\n createAudioContext() {\r\n const AudioContextConstructor =\r\n globalThis.AudioContext ?? (globalThis as any).webkitAudioContext;\r\n\r\n return new AudioContextConstructor();\r\n },\r\n loadPlayer: loadWebAudioFontPlayer,\r\n loadPreset: loadSoundfontPreset,\r\n createEngine: (context) => new WebAudioStrumSoundEngine(context),\r\n};\r\n\r\nconst SCHEDULER_INTERVAL_MS = 25;\r\nexport const SCHEDULE_AHEAD_SECONDS = 0.1;\r\n/** Head room between the press and the first stroke. */\r\nexport const START_DELAY_SECONDS = 0.05;\r\n/** Two times this close are the same instant. */\r\nconst TIME_EPSILON = 1e-6;\r\n/** The engine groups a pattern's notes, and a chord card's, apart. */\r\nexport const PATTERN_GROUP = 'pattern';\r\nexport const CHORD_GROUP = 'chord';\r\n\r\ninterface Playback {\r\n context: AudioContext;\r\n engine: StrumSoundEngine;\r\n preset: WebAudioFontPreset;\r\n instrument: StrumInstrumentId;\r\n sound: StrumSound;\r\n pattern: StrumPattern;\r\n chord: readonly number[];\r\n stepSeconds: number;\r\n startTime: number;\r\n /** How many step-lengths after the start the next step sounds. */\r\n nextTick: number;\r\n /** The 0-based step of the pattern that tick plays. */\r\n nextStep: number;\r\n}\r\n\r\n/** The notes one stroke scheduled — cancelled if a stop beats it. */\r\ninterface StrokeRecord {\r\n /** When its first note sounds. */\r\n time: number;\r\n /** When its last note sounds. */\r\n end: number;\r\n voices: StrumVoice[];\r\n}\r\n\r\nexport class StrumPlayer {\r\n onStep: ((patternId: string, stepNumber: number) => void) | null = null;\r\n onStateChange:\r\n | ((\r\n state: StrumPlayerState,\r\n patternId: string | null,\r\n error?: unknown,\r\n ) => void)\r\n | null = null;\r\n\r\n #runtime: StrumPlayerRuntime;\r\n #context: AudioContext | null = null;\r\n #engine: StrumSoundEngine | null = null;\r\n #playerLoad: Promise<WebAudioFontPlayerInstance> | null = null;\r\n #presetLoads = new Map<string, Promise<WebAudioFontPreset>>();\r\n #playback: Playback | null = null;\r\n #state: StrumPlayerState = 'idle';\r\n #statePatternId: string | null = null;\r\n #token = 0;\r\n #chordToken = 0;\r\n #records: StrokeRecord[] = [];\r\n #timer: ReturnType<typeof setInterval> | null = null;\r\n #uiTimers = new Set<ReturnType<typeof setTimeout>>();\r\n\r\n constructor(runtime: StrumPlayerRuntime = browserStrumPlayerRuntime) {\r\n this.#runtime = runtime;\r\n }\r\n\r\n get state(): StrumPlayerState {\r\n return this.#state;\r\n }\r\n\r\n /** The pattern loading or playing, if any. */\r\n get patternId(): string | null {\r\n return this.#statePatternId;\r\n }\r\n\r\n /**\r\n * Creates and resumes the audio context without playing anything. Browsers\r\n * only allow that from a user gesture, before its first `await` — so a\r\n * caller with its own async work to do before {@link play} calls this first.\r\n */\r\n unlock(): void {\r\n void this.#ensureContext().resume?.();\r\n }\r\n\r\n /**\r\n * Loads and decodes a soundfont ahead of a play. Creates the audio context,\r\n * so call it from a user gesture.\r\n */\r\n async preload(soundfont: StrumSoundfont): Promise<void> {\r\n const context = this.#ensureContext();\r\n const player = await this.#ensurePlayer();\r\n\r\n await this.#ensurePreset(player, context, soundfont);\r\n }\r\n\r\n /**\r\n * Stops whatever plays and starts `request`. Call it straight from the\r\n * user's gesture: the audio context is created and resumed before the first\r\n * `await`, as browsers require. Failures are reported through\r\n * `onStateChange`, not thrown.\r\n */\r\n async play(request: StrumPlaybackRequest): Promise<void> {\r\n this.stop();\r\n\r\n const token = ++this.#token;\r\n const context = this.#ensureContext();\r\n\r\n void context.resume?.();\r\n this.#setState('loading', request.pattern.id);\r\n\r\n try {\r\n const { soundfont } = getInstrument(request.instrument);\r\n const player = await this.#ensurePlayer();\r\n const preset = await this.#ensurePreset(player, context, soundfont);\r\n\r\n if (token === this.#token) {\r\n this.#start(request, context, preset);\r\n }\r\n } catch (error) {\r\n if (token === this.#token) {\r\n this.#setState('idle', null, error);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Plays a chord card's roll (`planChordRoll`). It sounds alongside a playing\r\n * pattern, and a stop leaves it ringing; the next roll damps it. Call it\r\n * from the user's gesture, like {@link play}.\r\n *\r\n * Of presses made while the soundfont loads, only the last one plays.\r\n * Resolves `true` once the notes are queued, `false` when a later press took\r\n * over; rejects when the player or soundfont fails to load.\r\n */\r\n async playChord(request: StrumChordRequest): Promise<boolean> {\r\n const token = ++this.#chordToken;\r\n const context = this.#ensureContext();\r\n\r\n void context.resume?.();\r\n\r\n const { soundfont } = getInstrument(request.instrument);\r\n const player = await this.#ensurePlayer();\r\n const preset = await this.#ensurePreset(player, context, soundfont);\r\n\r\n if (token !== this.#chordToken) {\r\n return false;\r\n }\r\n\r\n const engine = this.#ensureEngine(context);\r\n const sound = getStrumSound(request.instrument);\r\n const start = context.currentTime + START_DELAY_SECONDS;\r\n\r\n // The hand leaves the last roll's strings as it starts this one.\r\n engine.damp(CHORD_GROUP, start, sound.release);\r\n\r\n for (const note of planChordRoll(request.midi, sound, this.#random)) {\r\n this.#sound(engine, preset, sound, note, start, {\r\n slot: `${CHORD_GROUP}:${request.instrument}:${note.slot}`,\r\n group: CHORD_GROUP,\r\n });\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /** Swaps in an edited pattern mid-run; its next step plays the edit. */\r\n updatePattern(pattern: StrumPattern) {\r\n const playback = this.#playback;\r\n\r\n if (!playback || playback.pattern.id !== pattern.id) {\r\n return;\r\n }\r\n\r\n playback.pattern = pattern;\r\n\r\n if (playback.nextStep >= pattern.steps.length) {\r\n playback.nextStep = 0;\r\n }\r\n }\r\n\r\n stop() {\r\n this.#token += 1;\r\n\r\n const wasActive = this.#state !== 'idle';\r\n const playback = this.#playback;\r\n const now = this.#context?.currentTime ?? 0;\r\n let settle = now;\r\n\r\n this.#playback = null;\r\n this.#clearUiTimers();\r\n this.#stopTimer();\r\n\r\n for (const record of this.#records) {\r\n if (record.time > now + TIME_EPSILON) {\r\n record.voices.forEach((voice) => voice.cancel());\r\n } else {\r\n settle = Math.max(settle, record.end);\r\n }\r\n }\r\n\r\n this.#records = [];\r\n\r\n if (playback) {\r\n playback.engine.damp(PATTERN_GROUP, settle, playback.sound.release);\r\n }\r\n\r\n if (wasActive) {\r\n this.#setState('idle', null);\r\n }\r\n }\r\n\r\n #random = () => (this.#runtime.random ?? Math.random)();\r\n\r\n #ensureContext(): AudioContext {\r\n this.#context ??= this.#runtime.createAudioContext();\r\n return this.#context;\r\n }\r\n\r\n #ensureEngine(context: AudioContext): StrumSoundEngine {\r\n this.#engine ??= this.#runtime.createEngine(context);\r\n return this.#engine;\r\n }\r\n\r\n #ensurePlayer(): Promise<WebAudioFontPlayerInstance> {\r\n if (!this.#playerLoad) {\r\n const load = this.#runtime.loadPlayer().then((Player) => new Player());\r\n\r\n this.#playerLoad = load;\r\n load.catch(() => {\r\n if (this.#playerLoad === load) {\r\n this.#playerLoad = null;\r\n }\r\n });\r\n }\r\n\r\n return this.#playerLoad;\r\n }\r\n\r\n #ensurePreset(\r\n player: WebAudioFontPlayerInstance,\r\n context: BaseAudioContext,\r\n soundfont: StrumSoundfont,\r\n ): Promise<WebAudioFontPreset> {\r\n let load = this.#presetLoads.get(soundfont.key);\r\n\r\n if (!load) {\r\n const pending = this.#runtime.loadPreset(player, context, soundfont);\r\n\r\n load = pending;\r\n this.#presetLoads.set(soundfont.key, pending);\r\n pending.catch(() => {\r\n if (this.#presetLoads.get(soundfont.key) === pending) {\r\n this.#presetLoads.delete(soundfont.key);\r\n }\r\n });\r\n }\r\n\r\n return load;\r\n }\r\n\r\n #start(\r\n request: StrumPlaybackRequest,\r\n context: AudioContext,\r\n preset: WebAudioFontPreset,\r\n ) {\r\n this.#playback = {\r\n context,\r\n engine: this.#ensureEngine(context),\r\n preset,\r\n instrument: request.instrument,\r\n sound: getStrumSound(request.instrument),\r\n pattern: request.pattern,\r\n chord: [...request.chord],\r\n stepSeconds: getStrumStepSeconds(request.tempo, request.timeSignature),\r\n startTime: context.currentTime + START_DELAY_SECONDS,\r\n nextTick: 0,\r\n nextStep: 0,\r\n };\r\n\r\n this.#setState('playing', request.pattern.id);\r\n this.#timer ??= setInterval(() => this.#tick(), SCHEDULER_INTERVAL_MS);\r\n this.#tick();\r\n }\r\n\r\n #tick() {\r\n const playback = this.#playback;\r\n\r\n if (!playback || !playback.pattern.steps.length) {\r\n return;\r\n }\r\n\r\n const now = playback.context.currentTime;\r\n const horizon = now + SCHEDULE_AHEAD_SECONDS;\r\n let time = playback.startTime + playback.nextTick * playback.stepSeconds;\r\n\r\n while (time < horizon) {\r\n this.#playStep(playback, playback.nextStep, time);\r\n playback.nextTick += 1;\r\n playback.nextStep =\r\n (playback.nextStep + 1) % playback.pattern.steps.length;\r\n time = playback.startTime + playback.nextTick * playback.stepSeconds;\r\n }\r\n\r\n this.#records = this.#records.filter((record) => record.end > now);\r\n }\r\n\r\n #playStep(playback: Playback, index: number, time: number) {\r\n this.#showStep(playback, index + 1, time);\r\n\r\n const plan = planStroke(\r\n playback.chord,\r\n playback.pattern.steps[index],\r\n playback.sound,\r\n this.#random,\r\n );\r\n\r\n if (plan) {\r\n this.#perform(playback, plan, time);\r\n }\r\n }\r\n\r\n #perform(playback: Playback, plan: StrokePlan, time: number) {\r\n const onset = time + plan.shift;\r\n const record: StrokeRecord = { time: onset, end: onset, voices: [] };\r\n\r\n for (const note of plan.notes) {\r\n const voice = this.#sound(\r\n playback.engine,\r\n playback.preset,\r\n playback.sound,\r\n note,\r\n onset,\r\n {\r\n slot: `${PATTERN_GROUP}:${playback.instrument}:${note.slot}`,\r\n group: PATTERN_GROUP,\r\n },\r\n );\r\n\r\n record.end = Math.max(record.end, onset + note.offset);\r\n\r\n if (voice) {\r\n record.voices.push(voice);\r\n }\r\n }\r\n\r\n this.#records.push(record);\r\n }\r\n\r\n #sound(\r\n engine: StrumSoundEngine,\r\n preset: WebAudioFontPreset,\r\n sound: StrumSound,\r\n note: PlannedNote,\r\n start: number,\r\n { slot, group }: { slot: string; group: string },\r\n ): StrumVoice | null {\r\n return engine.play({\r\n preset,\r\n sound,\r\n pitch: note.pitch,\r\n time: start + note.offset,\r\n velocity: note.velocity,\r\n detune: note.detune,\r\n pan: note.pan,\r\n slot,\r\n group,\r\n });\r\n }\r\n\r\n #showStep(playback: Playback, stepNumber: number, time: number) {\r\n const delay = Math.max(0, (time - playback.context.currentTime) * 1000);\r\n const timer = setTimeout(() => {\r\n this.#uiTimers.delete(timer);\r\n\r\n if (playback === this.#playback) {\r\n this.onStep?.(playback.pattern.id, stepNumber);\r\n }\r\n }, delay);\r\n\r\n this.#uiTimers.add(timer);\r\n }\r\n\r\n #clearUiTimers() {\r\n this.#uiTimers.forEach((timer) => clearTimeout(timer));\r\n this.#uiTimers.clear();\r\n }\r\n\r\n #stopTimer() {\r\n if (this.#timer != null) {\r\n clearInterval(this.#timer);\r\n this.#timer = null;\r\n }\r\n }\r\n\r\n #setState(\r\n state: StrumPlayerState,\r\n patternId: string | null,\r\n error?: unknown,\r\n ) {\r\n this.#state = state;\r\n this.#statePatternId = patternId;\r\n this.onStateChange?.(state, patternId, error);\r\n }\r\n}\r\n\r\nfunction getInstrument(id: StrumInstrumentId): StrumInstrument {\r\n const instrument = getStrumInstrument(id);\r\n\r\n if (!instrument) {\r\n throw new Error(`Unknown instrument “${id}”.`);\r\n }\r\n\r\n return instrument;\r\n}\r\n","import {\r\n getChordCardMidi,\r\n loadChordDatabases,\r\n resolveChordCard,\r\n type ChordCard,\r\n type ChordDatabases,\r\n} from './chord-library.ts';\r\nimport {\r\n STRUM_INSTRUMENTS,\r\n getStrumInstrument,\r\n resolveStrumVoicing,\r\n type StrumInstrument,\r\n type StrumInstrumentId,\r\n type StrumVoicing,\r\n} from './instruments.ts';\r\nimport {\r\n parseOpenLyricPattern,\r\n type StrumPattern,\r\n type StrumPatternDefinition,\r\n} from './patterns.ts';\r\nimport {\r\n StrumPlayer,\r\n browserStrumPlayerRuntime,\r\n type StrumPlayerRuntime,\r\n type StrumPlayerState,\r\n} from './StrumPlayer.ts';\r\n\r\n/**\r\n * `OpenLyricPlayer` — the headless player: an Open Lyric song's strumming\r\n * patterns, and any chord Open Lyric can read, played on ten instruments —\r\n * acoustic, classical and electric guitar, piano, electric piano, organ,\r\n * ukulele, baritone ukulele, bass and upright bass. No DOM of its own;\r\n * `OpenLyricPlayerPlayground` is the UI built on it.\r\n *\r\n * It owns one {@link StrumPlayer} (one audio context, one soundfont cache) and\r\n * the chords-db tables the fretted voicings come from, loaded once on first\r\n * use. Every `play*` method must be called from a user gesture: the audio\r\n * context is unlocked before anything is awaited, as browsers require.\r\n */\r\n\r\nexport type OpenLyricPlayerState = StrumPlayerState;\r\n\r\nexport interface OpenLyricPlayerOptions {\r\n /** Browser seams (audio context, player + soundfont loaders); for tests. */\r\n runtime?: StrumPlayerRuntime;\r\n}\r\n\r\nexport interface OpenLyricPlayerStateEvent {\r\n state: OpenLyricPlayerState;\r\n /** The pattern loading or playing; `null` when idle. */\r\n patternId: string | null;\r\n /** Set when a play failed (the player script or soundfont did not load). */\r\n error?: unknown;\r\n}\r\n\r\nexport interface OpenLyricPlayerStepEvent {\r\n patternId: string;\r\n /** 1-based step of the pattern now sounding. */\r\n stepNumber: number;\r\n}\r\n\r\nexport interface OpenLyricPlayerEventMap {\r\n state: OpenLyricPlayerStateEvent;\r\n step: OpenLyricPlayerStepEvent;\r\n}\r\n\r\nexport interface OpenLyricPatternRequest {\r\n /**\r\n * The pattern: Open Lyric notation (`'d--u-- | d-u-d-'`), a\r\n * `{ id, label, source }` definition, or an already parsed pattern.\r\n */\r\n pattern: string | StrumPatternDefinition | StrumPattern;\r\n /** The song's key (`Config` → `Key`); a pattern strums its tonic chord. */\r\n key: string;\r\n /** Beats per minute, counting the time signature's unit. */\r\n tempo: number;\r\n /** Time signature (`Config` → `Time`). Default `4/4`. */\r\n time?: string;\r\n /** Default `guitar`. */\r\n instrument?: StrumInstrumentId;\r\n}\r\n\r\nconst DEFAULT_TIME = '4/4';\r\nconst DEFAULT_INSTRUMENT: StrumInstrumentId = 'guitar';\r\n\r\nfunction isParsedPattern(\r\n pattern: OpenLyricPatternRequest['pattern'],\r\n): pattern is StrumPattern {\r\n return typeof pattern === 'object' && Array.isArray(pattern.steps);\r\n}\r\n\r\nexport class OpenLyricPlayer {\r\n /** Every instrument, in display order. */\r\n static readonly instruments: readonly StrumInstrument[] = STRUM_INSTRUMENTS;\r\n\r\n #strum: StrumPlayer;\r\n #databases: ChordDatabases | null = null;\r\n #databaseLoad: Promise<ChordDatabases> | null = null;\r\n #listeners: {\r\n [K in keyof OpenLyricPlayerEventMap]: Set<\r\n (event: OpenLyricPlayerEventMap[K]) => void\r\n >;\r\n } = { state: new Set(), step: new Set() };\r\n\r\n constructor(options: OpenLyricPlayerOptions = {}) {\r\n this.#strum = new StrumPlayer(options.runtime ?? browserStrumPlayerRuntime);\r\n this.#strum.onStateChange = (state, patternId, error) =>\r\n this.#emit('state', { state, patternId, error });\r\n this.#strum.onStep = (patternId, stepNumber) =>\r\n this.#emit('step', { patternId, stepNumber });\r\n }\r\n\r\n get state(): OpenLyricPlayerState {\r\n return this.#strum.state;\r\n }\r\n\r\n /** The pattern loading or playing, if any. */\r\n get patternId(): string | null {\r\n return this.#strum.patternId;\r\n }\r\n\r\n /** Whether the chord tables have loaded — until then voicings are `null`. */\r\n get chordsLoaded(): boolean {\r\n return this.#databases !== null;\r\n }\r\n\r\n /** Subscribes to `state` or `step`; returns the unsubscribe. */\r\n on<K extends keyof OpenLyricPlayerEventMap>(\r\n type: K,\r\n listener: (event: OpenLyricPlayerEventMap[K]) => void,\r\n ): () => void {\r\n this.#listeners[type].add(listener);\r\n return () => {\r\n this.#listeners[type].delete(listener);\r\n };\r\n }\r\n\r\n /** Loads the chords-db tables once; every later call shares the load. */\r\n loadChords(): Promise<ChordDatabases> {\r\n if (!this.#databaseLoad) {\r\n const load = loadChordDatabases().then((databases) => {\r\n this.#databases = databases;\r\n return databases;\r\n });\r\n\r\n this.#databaseLoad = load;\r\n load.catch(() => {\r\n if (this.#databaseLoad === load) {\r\n this.#databaseLoad = null;\r\n }\r\n });\r\n }\r\n\r\n return this.#databaseLoad;\r\n }\r\n\r\n /**\r\n * The chord a pattern strums in `key` on `instrument` — the key's tonic\r\n * chord in that instrument's voicing. `null` when the key is unreadable,\r\n * the instrument has no voicing for it, or the chords have not loaded\r\n * ({@link loadChords}).\r\n */\r\n getVoicing(instrument: StrumInstrumentId, key: string): StrumVoicing | null {\r\n return this.#databases\r\n ? resolveStrumVoicing(instrument, key, this.#databases)\r\n : null;\r\n }\r\n\r\n /**\r\n * What `chord` looks like, and plays, on `instrument`: a chord diagram, piano\r\n * keys, or `none` with the reason. `null` until the chords have loaded.\r\n */\r\n resolveChord(instrument: StrumInstrumentId, chord: string): ChordCard | null {\r\n return this.#databases\r\n ? resolveChordCard(instrument, chord, this.#databases)\r\n : null;\r\n }\r\n\r\n /** Parses a pattern in Open Lyric notation against a time signature. */\r\n parsePattern(\r\n pattern: string | StrumPatternDefinition,\r\n time: string = DEFAULT_TIME,\r\n ): StrumPattern {\r\n const definition =\r\n typeof pattern === 'string'\r\n ? { id: 'pattern', label: 'Pattern', source: pattern }\r\n : pattern;\r\n\r\n return parseOpenLyricPattern(definition, time);\r\n }\r\n\r\n /**\r\n * Loads and decodes an instrument's soundfont ahead of a play. Creates the\r\n * audio context, so call it from a user gesture.\r\n */\r\n preload(instrument: StrumInstrumentId = DEFAULT_INSTRUMENT): Promise<void> {\r\n return this.#strum.preload(this.#getInstrument(instrument).soundfont);\r\n }\r\n\r\n /**\r\n * Stops whatever plays and loops `request.pattern` until {@link stop}.\r\n * Rejects when the pattern does not parse or there is nothing to strum (an\r\n * unreadable key, no voicing); a soundfont that fails to load is reported\r\n * through the `state` event instead, as `idle` with an `error`.\r\n */\r\n async playPattern(request: OpenLyricPatternRequest): Promise<void> {\r\n const time = request.time ?? DEFAULT_TIME;\r\n const instrument = this.#getInstrument(\r\n request.instrument ?? DEFAULT_INSTRUMENT,\r\n );\r\n const pattern = isParsedPattern(request.pattern)\r\n ? request.pattern\r\n : this.parsePattern(request.pattern, time);\r\n\r\n this.#strum.unlock();\r\n await this.loadChords();\r\n\r\n const voicing = this.getVoicing(instrument.id, request.key);\r\n\r\n if (!voicing) {\r\n throw new Error(\r\n `No ${instrument.label} voicing for the key of ${request.key}.`,\r\n );\r\n }\r\n\r\n await this.#strum.play({\r\n pattern,\r\n tempo: request.tempo,\r\n timeSignature: time,\r\n chord: voicing.midi,\r\n instrument: instrument.id,\r\n });\r\n }\r\n\r\n /**\r\n * Rolls `chord` once on `instrument`, alongside any playing pattern. Of\r\n * presses made while the soundfont loads only the last one plays: resolves\r\n * `true` once queued, `false` when a later press took over. Rejects when the\r\n * chord has nothing to play or the soundfont fails to load.\r\n */\r\n async playChord(\r\n chord: string,\r\n instrument: StrumInstrumentId = DEFAULT_INSTRUMENT,\r\n ): Promise<boolean> {\r\n const { label } = this.#getInstrument(instrument);\r\n\r\n this.#strum.unlock();\r\n\r\n const card = resolveChordCard(instrument, chord, await this.loadChords());\r\n const midi = getChordCardMidi(card);\r\n\r\n if (!midi.length) {\r\n throw new Error(\r\n card.kind === 'none' && card.reason === 'not-a-chord'\r\n ? `“${chord}” is not a chord Open Lyric can read.`\r\n : `No ${label} voicing for ${chord}.`,\r\n );\r\n }\r\n\r\n return this.#strum.playChord({ midi, instrument });\r\n }\r\n\r\n /** Swaps in an edited pattern mid-run; its next step plays the edit. */\r\n updatePattern(pattern: StrumPattern): void {\r\n this.#strum.updatePattern(pattern);\r\n }\r\n\r\n /**\r\n * Stops the pattern: a stroke under way finishes, then its ringing strings\r\n * are damped as a player's palm would. A chord roll rings on.\r\n */\r\n stop(): void {\r\n this.#strum.stop();\r\n }\r\n\r\n /** Drops every listener, then stops — silently. */\r\n destroy(): void {\r\n this.#listeners.state.clear();\r\n this.#listeners.step.clear();\r\n this.stop();\r\n }\r\n\r\n #getInstrument(id: StrumInstrumentId): StrumInstrument {\r\n const instrument = getStrumInstrument(id);\r\n\r\n if (!instrument) {\r\n throw new Error(`Unknown instrument “${id}”.`);\r\n }\r\n\r\n return instrument;\r\n }\r\n\r\n #emit<K extends keyof OpenLyricPlayerEventMap>(\r\n type: K,\r\n event: OpenLyricPlayerEventMap[K],\r\n ): void {\r\n for (const listener of this.#listeners[type]) {\r\n listener(event);\r\n }\r\n }\r\n}\r\n","import type { PianoVoicing } from './chord-library.ts';\r\nimport { getMidiNoteName } from './instruments.ts';\r\n\r\n/**\r\n * The piano figure a chord card draws — shared by the playground's chord card\r\n * and the chord popup's instrument view (`OpenLyricPluginPlayer`). Styled by\r\n * the `.piano-board*` classes in each surface's sheet.\r\n */\r\n\r\nconst WHITE_KEY_WIDTH = 18;\r\nconst WHITE_KEY_HEIGHT = 66;\r\nconst BLACK_KEY_WIDTH = 12;\r\nconst BLACK_KEY_HEIGHT = 40;\r\nconst BLACK_CHROMAS = new Set([1, 3, 6, 8, 10]);\r\nconst SVG_NS = 'http://www.w3.org/2000/svg';\r\n\r\nfunction createSvgElement<K extends keyof SVGElementTagNameMap>(\r\n name: K,\r\n attributes: Record<string, string | number>,\r\n): SVGElementTagNameMap[K] {\r\n const element = document.createElementNS(SVG_NS, name);\r\n\r\n for (const [attribute, value] of Object.entries(attributes)) {\r\n element.setAttribute(attribute, String(value));\r\n }\r\n\r\n return element;\r\n}\r\n\r\n/** A keyboard over the chord's octaves, its keys lit and named. */\r\nexport function createPianoBoard(\r\n chord: string,\r\n piano: PianoVoicing,\r\n keyText: string,\r\n) {\r\n const keys = Array.from(\r\n { length: piano.highest - piano.lowest + 1 },\r\n (_, index) => piano.lowest + index,\r\n );\r\n const whites = keys.filter((midi) => !BLACK_CHROMAS.has(midi % 12));\r\n const width = whites.length * WHITE_KEY_WIDTH;\r\n const svg = createSvgElement('svg', {\r\n class: 'piano-board',\r\n viewBox: `0 0 ${width + 1} ${WHITE_KEY_HEIGHT + 1}`,\r\n role: 'img',\r\n 'aria-label': `Piano keys for ${chord}: ${piano.midi.map((midi) => getMidiNoteName(midi, keyText)).join(' ')}`,\r\n });\r\n const drawKey = (midi: number, x: number, black: boolean) => {\r\n const lit = piano.midi.includes(midi);\r\n\r\n svg.append(\r\n createSvgElement('rect', {\r\n class: `piano-board__${black ? 'black' : 'white'}${lit ? ' is-lit' : ''}`,\r\n x: x + 0.5,\r\n y: 0.5,\r\n width: black ? BLACK_KEY_WIDTH : WHITE_KEY_WIDTH,\r\n height: black ? BLACK_KEY_HEIGHT : WHITE_KEY_HEIGHT,\r\n rx: 2,\r\n }),\r\n );\r\n\r\n if (lit) {\r\n const label = createSvgElement('text', {\r\n class: 'piano-board__label',\r\n x: x + 0.5 + (black ? BLACK_KEY_WIDTH : WHITE_KEY_WIDTH) / 2,\r\n y: black ? BLACK_KEY_HEIGHT - 6 : WHITE_KEY_HEIGHT - 8,\r\n 'text-anchor': 'middle',\r\n });\r\n\r\n label.textContent = getMidiNoteName(midi, keyText).replace(/-?\\d+$/, '');\r\n svg.append(label);\r\n }\r\n };\r\n\r\n whites.forEach((midi, index) =>\r\n drawKey(midi, index * WHITE_KEY_WIDTH, false),\r\n );\r\n keys\r\n .filter((midi) => BLACK_CHROMAS.has(midi % 12))\r\n .forEach((midi) => {\r\n const whitesBelow = whites.filter((white) => white < midi).length;\r\n\r\n drawKey(midi, whitesBelow * WHITE_KEY_WIDTH - BLACK_KEY_WIDTH / 2, true);\r\n });\r\n\r\n return svg;\r\n}\r\n","import { SVGuitarChord } from 'svguitar';\r\n\r\nimport playerPluginCss from './OpenLyricPluginPlayer.scss?inline';\r\nimport { createPianoBoard } from './chord-figure.ts';\r\nimport { getChordCardMidi, type ChordCard } from './chord-library.ts';\r\nimport {\r\n OpenLyricPlayer,\r\n type OpenLyricPlayerStateEvent,\r\n type OpenLyricPlayerStepEvent,\r\n} from './OpenLyricPlayer.ts';\r\nimport {\r\n getMidiNoteName,\r\n getStrumInstrument,\r\n type StrumInstrumentId,\r\n} from './instruments.ts';\r\nimport type {\r\n OpenLyricChordPopupEventDetail,\r\n OpenLyricComponentHost,\r\n OpenLyricContributions,\r\n OpenLyricPlugin,\r\n OpenLyricRenderContext,\r\n OpenLyricStyleContribution,\r\n OpenLyricSurface,\r\n} from '../../editor/components/index.ts';\r\n\r\n/**\r\n * `OpenLyricPluginPlayer` — the strum player as a plugin for the two preview\r\n * components, `OpenLyric` (`lyric`) and `OpenLyricMarkdownManager`\r\n * (`markdown`):\r\n *\r\n * - every strumming-pattern card gets a play/stop button, and the step\r\n * sounding is marked on the card while it plays;\r\n * - a click on a chord (a key note) rolls it, and so does the ▶ the chord\r\n * popup gets; beside it, a view toggle shows the chord on the instrument\r\n * (its diagram, or piano keys) in place of the popup's guitar diagram. A\r\n * strumming-pattern reference's popup gets a ▶ that plays its pattern;\r\n * - the preview's settings popup gets an instrument picker\r\n * (`contributes.settings`).\r\n *\r\n * Attach one instance per component. Every instance plays through one\r\n * `OpenLyricPlayer` shared by the page unless it is handed its own, so one\r\n * pattern sounds at a time across the embeds.\r\n */\r\n\r\nexport interface OpenLyricPluginPlayerOptions {\r\n /**\r\n * The instrument to play on until a reader picks another in the settings\r\n * popup, and what Reset returns to. Default `guitar`.\r\n */\r\n instrument?: StrumInstrumentId;\r\n /** The player to sound through. Default: one shared by the page. */\r\n player?: OpenLyricPlayer;\r\n /**\r\n * localStorage key remembering the instrument a reader picked. Default\r\n * `openLyricPluginPlayer.instrument`; `null` remembers nothing.\r\n */\r\n storageKey?: string | null;\r\n}\r\n\r\ntype PatternState = 'idle' | 'loading' | 'playing';\r\n\r\n/** A pattern card's play request, read off its data attributes. */\r\ninterface PatternCardRequest {\r\n id: string;\r\n /** The card's `data-pattern-index`. */\r\n index: string;\r\n label: string;\r\n source: string;\r\n key: string;\r\n tempo: number;\r\n time: string;\r\n}\r\n\r\n/** The chord popup's view toggle while it shows a chord of this plugin's. */\r\ninterface ChordView {\r\n chord: string;\r\n button: HTMLElement;\r\n panel: HTMLElement;\r\n /**\r\n * The view's frame in the panel, kept while it shows so the instrument\r\n * arrows survive a redraw: dropping the focused one would blur the popup,\r\n * and a blurred popup closes.\r\n */\r\n shell: {\r\n view: HTMLElement;\r\n caption: HTMLElement;\r\n figure: HTMLElement;\r\n notes: HTMLElement;\r\n } | null;\r\n}\r\n\r\n/** One component this plugin is attached to. */\r\ninterface Session {\r\n /** Prefix of the pattern ids this session plays. */\r\n uid: string;\r\n root: HTMLElement | null;\r\n /** Takes the root's listeners and observer back off. */\r\n unbindRoot: (() => void) | null;\r\n /**\r\n * The ▶ in the chord popup while it shows something of this session's —\r\n * with the pattern card's index when it is a pattern reference's.\r\n */\r\n popupButton: { button: HTMLElement; index: string | null } | null;\r\n /** The last pattern played here, and the instrument it sounds on. */\r\n request: (PatternCardRequest & { instrument: StrumInstrumentId }) | null;\r\n state: PatternState;\r\n}\r\n\r\nconst PLUGIN_ID = 'player';\r\nconst DEFAULT_INSTRUMENT: StrumInstrumentId = 'guitar';\r\nconst DEFAULT_STORAGE_KEY = 'openLyricPluginPlayer.instrument';\r\n/** What a pattern strums when the song has no `Key`. */\r\nconst DEFAULT_KEY = 'C';\r\n/** The playground's default tempo, for a song without a `Tempo`. */\r\nconst DEFAULT_TEMPO = 70;\r\nconst MIN_TEMPO = 20;\r\nconst MAX_TEMPO = 300;\r\nconst DEFAULT_TIME = '4/4';\r\n\r\n/** The marker exports strip (`readExportMarkup` in the core). */\r\nconst PLUGIN_UI_ATTRIBUTE = 'data-ol-plugin-ui';\r\n/** Scopes this plugin's stylesheet to a render root it decorates. */\r\nconst ROOT_MARKER_ATTRIBUTE = 'data-ol-player';\r\nconst CARD_STATE_ATTRIBUTE = 'data-olpp-state';\r\nconst STEP_ATTRIBUTE = 'data-olpp-step';\r\n/** How `PluginHost` marks the style nodes it installs for a plugin. */\r\nconst PLUGIN_STYLE_MARKER = 'data-open-lyric-plugin-id';\r\n\r\nconst PATTERN_CARD_SELECTOR = '[data-pattern-source][data-pattern-index]';\r\nconst CHORD_SELECTOR = '[data-chord-key]';\r\n/** `OPEN_LYRIC_CHORD_POPUP_EVENT` in the core: the chord popup opening. */\r\nconst CHORD_POPUP_EVENT = 'open-lyric:chord-popup';\r\nconst PLAY_BUTTON_SELECTOR = `.olpp-play[${PLUGIN_UI_ATTRIBUTE}=\"${PLUGIN_ID}\"]`;\r\nconst STEP_ROW_SELECTOR =\r\n '.ol-song-view__pattern-row--stroke, .ol-song-view__pattern-row--count';\r\n\r\n/** The zero-width separators a Khmer document may put between groups. */\r\nconst INVISIBLE_SEPARATOR_RE = /[\\u200B-\\u200D\\u2060\\uFEFF]/g;\r\n\r\nconst VIEW_ICON =\r\n '<svg class=\"olpp-view-toggle__icon\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M2 12s3.6-6.5 10-6.5S22 12 22 12s-3.6 6.5-10 6.5S2 12 2 12z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\"/><circle cx=\"12\" cy=\"12\" r=\"3.2\" fill=\"currentColor\"/></svg>';\r\n\r\n/**\r\n * Inlined (`?inline`) rather than contributed as an `href`, so the sheet\r\n * travels inside the bundle and needs no second request.\r\n *\r\n * A `<link href>` pointing at an emitted `.css` asset is the one thing that\r\n * cannot be relied on here: a Vite dev host serves a `.css` under\r\n * `node_modules` as a JavaScript HMR module (`Content-Type: text/javascript`),\r\n * and a browser refuses a stylesheet whose MIME is not `text/css` — so every\r\n * control this sheet styles, the chord popup's ▶ among them, rendered as a\r\n * bare unstyled `<button>`. `cssText` has no MIME to get wrong.\r\n */\r\nconst PLAYER_STYLE: OpenLyricStyleContribution = { cssText: playerPluginCss };\r\n\r\nlet sharedPlayer: OpenLyricPlayer | null = null;\r\nlet sessionCount = 0;\r\nlet fieldCount = 0;\r\n\r\nfunction getSharedPlayer(): OpenLyricPlayer {\r\n sharedPlayer ??= new OpenLyricPlayer();\r\n return sharedPlayer;\r\n}\r\n\r\nfunction isInstrumentId(value: unknown): value is StrumInstrumentId {\r\n return (\r\n typeof value === 'string' && Boolean(getStrumInstrument(value as never))\r\n );\r\n}\r\n\r\nfunction readTempo(text: string | undefined): number {\r\n const match = String(text ?? '').match(/\\d+(?:\\.\\d+)?/);\r\n const tempo = match ? Math.round(Number(match[0])) : Number.NaN;\r\n return Number.isFinite(tempo) && tempo > 0\r\n ? Math.min(MAX_TEMPO, Math.max(MIN_TEMPO, tempo))\r\n : DEFAULT_TEMPO;\r\n}\r\n\r\nfunction readPatternCard(card: HTMLElement, uid: string): PatternCardRequest {\r\n const index = card.dataset['patternIndex'] ?? '';\r\n const number = Number.parseInt(index, 10) + 1;\r\n const numberText = Number.isFinite(number) ? `#${number}` : '';\r\n const label = (card.dataset['patternLabel'] ?? '').trim();\r\n\r\n return {\r\n id: `${uid}:${index}`,\r\n index,\r\n label:\r\n label && label !== `Pattern ${numberText}`\r\n ? `${numberText} ${label}`.trim()\r\n : `Pattern ${numberText}`.trim(),\r\n source: (card.dataset['patternSource'] ?? '')\r\n .replace(INVISIBLE_SEPARATOR_RE, ' ')\r\n .trim(),\r\n key: (card.dataset['patternPreviewKey'] ?? '').trim() || DEFAULT_KEY,\r\n tempo: readTempo(card.dataset['patternPreviewTempo']),\r\n time: (card.dataset['patternPreviewTime'] ?? '').trim() || DEFAULT_TIME,\r\n };\r\n}\r\n\r\nfunction findPatternCards(root: ParentNode): HTMLElement[] {\r\n return Array.from(root.querySelectorAll<HTMLElement>(PATTERN_CARD_SELECTOR));\r\n}\r\n\r\nfunction findPatternCard(root: ParentNode, index: string): HTMLElement | null {\r\n return (\r\n findPatternCards(root).find(\r\n (card) => card.dataset['patternIndex'] === index,\r\n ) ?? null\r\n );\r\n}\r\n\r\nfunction clearSteps(scope: ParentNode): void {\r\n for (const cell of Array.from(\r\n scope.querySelectorAll(`[${STEP_ATTRIBUTE}]`),\r\n )) {\r\n cell.removeAttribute(STEP_ATTRIBUTE);\r\n }\r\n}\r\n\r\nexport class OpenLyricPluginPlayer implements OpenLyricPlugin {\r\n readonly id = PLUGIN_ID;\r\n readonly apiVersion = 1 as const;\r\n readonly surfaces: readonly OpenLyricSurface[] = ['lyric', 'markdown'];\r\n readonly contributes: OpenLyricContributions;\r\n\r\n /**\r\n * Put the plugin's stylesheet in `document.head` for the page's lifetime,\r\n * without attaching the plugin — the page-level counterpart of the sheet an\r\n * attached plugin installs into its component. Idempotent. The sheet only\r\n * styles render roots the plugin has marked, so it changes nothing on a page\r\n * that never attaches it.\r\n */\r\n static installShellStyle(): void {\r\n const head = globalThis.document?.head;\r\n if (!head || !PLAYER_STYLE.cssText) {\r\n return;\r\n }\r\n // One sheet per page: the marker alone identifies it, the stylesheet\r\n // being inline now rather than an href to compare.\r\n if (head.querySelector(`style[${PLUGIN_STYLE_MARKER}=\"${PLUGIN_ID}\"]`)) {\r\n return;\r\n }\r\n const style = Object.assign(document.createElement('style'), {\r\n textContent: PLAYER_STYLE.cssText,\r\n });\r\n style.setAttribute(PLUGIN_STYLE_MARKER, PLUGIN_ID);\r\n head.append(style);\r\n }\r\n\r\n #player: OpenLyricPlayer | null;\r\n #playerUnsubscribes: Array<() => void> = [];\r\n #defaultInstrument: StrumInstrumentId;\r\n #instrument: StrumInstrumentId;\r\n #storageKey: string | null;\r\n #sessions = new Map<OpenLyricComponentHost, Session>();\r\n #instrumentSelects = new Set<HTMLSelectElement>();\r\n /** Whether the chord popup shows the instrument view; kept across popups. */\r\n #chordViewOpen = false;\r\n #chordView: ChordView | null = null;\r\n\r\n constructor(options: OpenLyricPluginPlayerOptions = {}) {\r\n if (\r\n options.instrument !== undefined &&\r\n !isInstrumentId(options.instrument)\r\n ) {\r\n throw new Error(`Unknown instrument “${String(options.instrument)}”.`);\r\n }\r\n this.#defaultInstrument = options.instrument ?? DEFAULT_INSTRUMENT;\r\n this.#storageKey =\r\n options.storageKey === undefined\r\n ? DEFAULT_STORAGE_KEY\r\n : options.storageKey;\r\n // A reader's saved pick outranks the host's configured instrument.\r\n this.#instrument = this.#readStoredInstrument() ?? this.#defaultInstrument;\r\n this.#player = options.player ?? null;\r\n this.contributes = {\r\n style: [PLAYER_STYLE],\r\n settings: [\r\n {\r\n render: (mount) => this.#renderInstrumentField(mount),\r\n reset: () => this.resetInstrument(),\r\n },\r\n ],\r\n };\r\n // `contributes.style` alone is not enough for this plugin, and the reason\r\n // is structural rather than a timing accident: the chord popup is appended\r\n // to `document.body` by the core, NOT into the component container that a\r\n // scoped contribution installs into. A host that never calls\r\n // `installShellStyle()` therefore got a popup ▶ with no rules reaching it —\r\n // a bare unstyled <button>, 16×6 px, its triangle missing because the icon\r\n // is drawn entirely in CSS. So the plugin installs its own page-level sheet\r\n // instead of relying on the host to remember. It is idempotent, and safe to\r\n // install unasked for the same reason `installShellStyle` is: every rule is\r\n // scoped under markup only this plugin writes, so a page that ends up\r\n // attaching nothing is unchanged by it.\r\n OpenLyricPluginPlayer.installShellStyle();\r\n }\r\n\r\n /** The player patterns and chords sound through. */\r\n get player(): OpenLyricPlayer {\r\n this.#player ??= getSharedPlayer();\r\n return this.#player;\r\n }\r\n\r\n /** The instrument patterns and chords play on. */\r\n get instrument(): StrumInstrumentId {\r\n return this.#instrument;\r\n }\r\n\r\n /**\r\n * Switch instruments: remembered, reflected in every settings popup showing\r\n * the picker, and a pattern playing here restarts on the new one.\r\n */\r\n set instrument(next: StrumInstrumentId) {\r\n if (!isInstrumentId(next)) {\r\n throw new Error(`Unknown instrument “${String(next)}”.`);\r\n }\r\n this.#writeStoredInstrument(next);\r\n this.#applyInstrument(next);\r\n }\r\n\r\n /** Back to the configured instrument, forgetting the reader's pick. */\r\n resetInstrument(): void {\r\n this.#writeStoredInstrument(null);\r\n this.#applyInstrument(this.#defaultInstrument);\r\n }\r\n\r\n /** Stop a pattern this plugin started, wherever it plays. */\r\n stop(): void {\r\n for (const session of this.#sessions.values()) {\r\n this.#stopSession(session);\r\n }\r\n }\r\n\r\n /* ------------------------------ lifecycle ------------------------------ */\r\n\r\n install(host: OpenLyricComponentHost): void {\r\n this.#session(host);\r\n }\r\n\r\n uninstall(host: OpenLyricComponentHost): void {\r\n const session = this.#sessions.get(host);\r\n if (session) {\r\n this.#detachSession(session);\r\n this.#sessions.delete(host);\r\n }\r\n if (!this.#sessions.size) {\r\n for (const off of this.#playerUnsubscribes.splice(0)) {\r\n off();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Attached to a preview that is already mounted, nothing has rendered with\r\n * this plugin yet — render once so the cards get their buttons.\r\n */\r\n onMount(host: OpenLyricComponentHost): void {\r\n if (!this.#session(host).root) {\r\n (host as Partial<{ reload(): void }>).reload?.();\r\n }\r\n }\r\n\r\n onUnmount(host: OpenLyricComponentHost): void {\r\n const session = this.#sessions.get(host);\r\n if (session) {\r\n this.#detachSession(session);\r\n }\r\n }\r\n\r\n onAfterRender(\r\n host: OpenLyricComponentHost,\r\n context: OpenLyricRenderContext,\r\n ): void {\r\n const session = this.#session(host);\r\n const root = context.root;\r\n\r\n if (session.root !== root) {\r\n session.unbindRoot?.();\r\n session.root = root;\r\n session.unbindRoot = this.#bindRoot(session, root);\r\n }\r\n this.#decorate(session);\r\n }\r\n\r\n /* ------------------------------- sessions ------------------------------ */\r\n\r\n #session(host: OpenLyricComponentHost): Session {\r\n let session = this.#sessions.get(host);\r\n if (!session) {\r\n sessionCount += 1;\r\n session = {\r\n uid: `olpp-${sessionCount}`,\r\n root: null,\r\n unbindRoot: null,\r\n popupButton: null,\r\n request: null,\r\n state: 'idle',\r\n };\r\n this.#sessions.set(host, session);\r\n }\r\n return session;\r\n }\r\n\r\n /** Stop the session's pattern and take everything added back off its root. */\r\n #detachSession(session: Session): void {\r\n this.#stopSession(session);\r\n session.unbindRoot?.();\r\n session.unbindRoot = null;\r\n session.popupButton?.button.remove();\r\n session.popupButton = null;\r\n\r\n const root = session.root;\r\n if (root) {\r\n root.removeAttribute(ROOT_MARKER_ATTRIBUTE);\r\n for (const button of Array.from(\r\n root.querySelectorAll(PLAY_BUTTON_SELECTOR),\r\n )) {\r\n button.remove();\r\n }\r\n for (const card of Array.from(\r\n root.querySelectorAll(`[${CARD_STATE_ATTRIBUTE}]`),\r\n )) {\r\n card.removeAttribute(CARD_STATE_ATTRIBUTE);\r\n }\r\n clearSteps(root);\r\n }\r\n session.root = null;\r\n }\r\n\r\n #stopSession(session: Session): void {\r\n // Forgetting the request also cancels a play still loading for it: when\r\n // it starts, `#handleState` finds it stale and stops it.\r\n session.request = null;\r\n if (session.state === 'idle') {\r\n return;\r\n }\r\n session.state = 'idle';\r\n if (this.#ownsPattern(session, this.#player?.patternId)) {\r\n this.#player?.stop();\r\n }\r\n this.#paint(session);\r\n }\r\n\r\n #ownsPattern(session: Session, patternId: string | null | undefined) {\r\n return Boolean(patternId?.startsWith(`${session.uid}:`));\r\n }\r\n\r\n /**\r\n * After a re-render: keep a playing pattern in step with its card. An edited\r\n * pattern swaps in mid-run; a changed key, tempo or time restarts it; a card\r\n * that is gone stops it.\r\n */\r\n #followRender(session: Session): void {\r\n const request = session.request;\r\n if (session.state === 'idle' || !request || !session.root) {\r\n return;\r\n }\r\n\r\n const card = findPatternCard(session.root, request.index);\r\n if (!card) {\r\n this.#stopSession(session);\r\n return;\r\n }\r\n\r\n const next = readPatternCard(card, session.uid);\r\n if (\r\n next.key !== request.key ||\r\n next.tempo !== request.tempo ||\r\n next.time !== request.time\r\n ) {\r\n this.#playCard(session, card);\r\n return;\r\n }\r\n if (next.source !== request.source) {\r\n try {\r\n this.player.updatePattern(\r\n this.player.parsePattern(\r\n { id: next.id, label: next.label, source: next.source },\r\n next.time,\r\n ),\r\n );\r\n session.request = { ...request, ...next };\r\n } catch {\r\n this.#stopSession(session);\r\n }\r\n }\r\n }\r\n\r\n /* -------------------------------- player ------------------------------- */\r\n\r\n #ensurePlayer(): OpenLyricPlayer {\r\n const player = this.player;\r\n if (!this.#playerUnsubscribes.length) {\r\n this.#playerUnsubscribes.push(\r\n player.on('state', (event) => this.#handleState(event)),\r\n player.on('step', (event) => this.#handleStep(event)),\r\n );\r\n }\r\n return player;\r\n }\r\n\r\n #handleState({ state, patternId, error }: OpenLyricPlayerStateEvent): void {\r\n let isStale = false;\r\n for (const session of this.#sessions.values()) {\r\n const previous = session.state;\r\n const isOwn = this.#ownsPattern(session, patternId);\r\n if (state !== 'idle' && isOwn && patternId !== session.request?.id) {\r\n // A play this session has since stopped or replaced, starting late\r\n // (its chord tables were still loading): silenced below.\r\n isStale = true;\r\n continue;\r\n }\r\n if (state !== 'idle' && isOwn) {\r\n session.state = state;\r\n } else {\r\n // Anything else ends this session's pattern: a stop, a failed load, or\r\n // another pattern taking over the player. The stop inside a new play\r\n // lands here too, and that play's own `loading` follows at once.\r\n if (error && previous === 'loading') {\r\n console.error('Open Lyric player failed to load a sound.', error);\r\n }\r\n session.state = 'idle';\r\n }\r\n if (session.state !== previous) {\r\n this.#paint(session);\r\n }\r\n }\r\n if (isStale) {\r\n this.#player?.stop();\r\n }\r\n }\r\n\r\n #handleStep({ patternId, stepNumber }: OpenLyricPlayerStepEvent): void {\r\n for (const session of this.#sessions.values()) {\r\n const request = session.request;\r\n if (!request || request.id !== patternId || !session.root) {\r\n continue;\r\n }\r\n const card = findPatternCard(session.root, request.index);\r\n if (!card) {\r\n continue;\r\n }\r\n clearSteps(card);\r\n for (const row of Array.from(card.querySelectorAll(STEP_ROW_SELECTOR))) {\r\n row.children[stepNumber - 1]?.setAttribute(STEP_ATTRIBUTE, '');\r\n }\r\n }\r\n }\r\n\r\n #togglePattern(session: Session, card: HTMLElement): void {\r\n const index = card.dataset['patternIndex'] ?? '';\r\n if (session.state !== 'idle' && session.request?.index === index) {\r\n this.#stopSession(session);\r\n return;\r\n }\r\n this.#playCard(session, card);\r\n }\r\n\r\n /** Play the card's pattern — straight from the click, as audio requires. */\r\n #playCard(session: Session, card: HTMLElement): void {\r\n const request = {\r\n ...readPatternCard(card, session.uid),\r\n instrument: this.#instrument,\r\n };\r\n const player = this.#ensurePlayer();\r\n\r\n session.request = request;\r\n session.state = 'loading';\r\n this.#paint(session);\r\n\r\n player\r\n .playPattern({\r\n pattern: {\r\n id: request.id,\r\n label: request.label,\r\n source: request.source,\r\n },\r\n key: request.key,\r\n tempo: request.tempo,\r\n time: request.time,\r\n instrument: request.instrument,\r\n })\r\n .catch((error: unknown) => {\r\n if (session.request === request && session.state !== 'idle') {\r\n session.state = 'idle';\r\n this.#paint(session);\r\n }\r\n console.error(\r\n `Open Lyric player could not play ${request.label}.`,\r\n error,\r\n );\r\n });\r\n }\r\n\r\n #playChord(chord: string): void {\r\n this.#ensurePlayer()\r\n .playChord(chord, this.#instrument)\r\n .catch((error: unknown) => {\r\n console.warn(`Open Lyric player could not play “${chord}”.`, error);\r\n });\r\n }\r\n\r\n #applyInstrument(next: StrumInstrumentId): void {\r\n for (const select of this.#instrumentSelects) {\r\n select.value = next;\r\n }\r\n if (next === this.#instrument) {\r\n return;\r\n }\r\n this.#instrument = next;\r\n this.#renderChordView();\r\n\r\n for (const session of this.#sessions.values()) {\r\n const card =\r\n session.state !== 'idle' && session.request && session.root\r\n ? findPatternCard(session.root, session.request.index)\r\n : null;\r\n if (card) {\r\n this.#playCard(session, card);\r\n } else {\r\n // The idle buttons name the instrument too.\r\n this.#paint(session);\r\n }\r\n }\r\n }\r\n\r\n /* --------------------------------- DOM --------------------------------- */\r\n\r\n /**\r\n * Give every pattern card on the root its button and mirror the playing\r\n * state onto it. Runs after each render, and whenever the root's content\r\n * changes without one: an app page's incremental fast path patches the\r\n * panel itself.\r\n */\r\n #decorate(session: Session): void {\r\n const root = session.root;\r\n if (!root) {\r\n return;\r\n }\r\n root.setAttribute(ROOT_MARKER_ATTRIBUTE, '');\r\n for (const card of findPatternCards(root)) {\r\n if (!card.querySelector(PLAY_BUTTON_SELECTOR)) {\r\n this.#addPlayButton(session, card);\r\n }\r\n }\r\n this.#followRender(session);\r\n this.#paint(session);\r\n }\r\n\r\n #createPlayButton(onPress: () => void): HTMLButtonElement {\r\n const button = document.createElement('button');\r\n button.type = 'button';\r\n button.className = 'olpp-play';\r\n button.setAttribute(PLUGIN_UI_ATTRIBUTE, PLUGIN_ID);\r\n button.dataset['state'] = 'idle';\r\n button.innerHTML =\r\n '<span class=\"olpp-play__icon\" aria-hidden=\"true\"></span>';\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault();\r\n // The card itself can be a host's click target (the markdown panel\r\n // jumps to the pattern's source line); a press here only plays.\r\n event.stopPropagation();\r\n onPress();\r\n });\r\n return button;\r\n }\r\n\r\n #addPlayButton(session: Session, card: HTMLElement): void {\r\n const button = this.#createPlayButton(() =>\r\n this.#togglePattern(session, card),\r\n );\r\n const slot =\r\n card.querySelector('.ol-song-view__pattern-heading') ??\r\n card.querySelector('.ol-song-view__pattern-header') ??\r\n card;\r\n slot.prepend(button);\r\n }\r\n\r\n /** Chord clicks, the chord popup opening, and content swapped in. */\r\n #bindRoot(session: Session, root: HTMLElement): () => void {\r\n const onClick = (event: Event) => {\r\n const target = event.target instanceof Element ? event.target : null;\r\n const chord = target?.closest<HTMLElement>(CHORD_SELECTOR);\r\n if (!chord || !root.contains(chord)) {\r\n return;\r\n }\r\n const name = (chord.getAttribute('data-chord-key') ?? '').trim();\r\n if (name) {\r\n this.#playChord(name);\r\n }\r\n };\r\n const onPopup = (event: Event) => this.#fillPopupActions(session, event);\r\n const observer =\r\n typeof MutationObserver === 'function'\r\n ? new MutationObserver(() => this.#decorate(session))\r\n : null;\r\n\r\n root.addEventListener('click', onClick);\r\n root.addEventListener(CHORD_POPUP_EVENT, onPopup);\r\n observer?.observe(root, { childList: true, subtree: true });\r\n return () => {\r\n root.removeEventListener('click', onClick);\r\n root.removeEventListener(CHORD_POPUP_EVENT, onPopup);\r\n observer?.disconnect();\r\n };\r\n }\r\n\r\n /**\r\n * The chord popup opened for something in this preview: put a ▶ in its\r\n * header. A chord's rolls the chord; a pattern reference's plays or stops\r\n * the pattern of the card it refers to.\r\n */\r\n #fillPopupActions(session: Session, event: Event): void {\r\n const detail = (event as CustomEvent<OpenLyricChordPopupEventDetail>)\r\n .detail;\r\n const trigger = event.target instanceof Element ? event.target : null;\r\n if (!detail?.actions || !trigger) {\r\n return;\r\n }\r\n\r\n if (detail.kind === 'chord') {\r\n const chord = detail.text;\r\n const button = this.#createPlayButton(() => this.#playChord(chord));\r\n const text = `Play ${chord} on ${this.#instrumentLabel()}`;\r\n button.setAttribute('aria-label', text);\r\n button.title = text;\r\n session.popupButton = { button, index: null };\r\n detail.actions.append(button);\r\n // An older core's popup has no panel to show the view in.\r\n if (detail.panel) {\r\n detail.actions.append(this.#createViewToggle(chord, detail.panel));\r\n this.#renderChordView();\r\n }\r\n return;\r\n }\r\n\r\n const reference = Number.parseInt(\r\n trigger.getAttribute('data-pattern-reference-index') ?? '',\r\n 10,\r\n );\r\n const index = String(reference - 1);\r\n if (!session.root || !findPatternCard(session.root, index)) {\r\n return;\r\n }\r\n const button = this.#createPlayButton(() => {\r\n const card = session.root ? findPatternCard(session.root, index) : null;\r\n if (card) {\r\n this.#togglePattern(session, card);\r\n }\r\n });\r\n session.popupButton = { button, index };\r\n detail.actions.append(button);\r\n this.#paint(session);\r\n }\r\n\r\n /**\r\n * Mirror the session's state onto its cards, their buttons, and a pattern\r\n * reference's ▶ in the chord popup.\r\n */\r\n #paint(session: Session): void {\r\n const root = session.root;\r\n if (!root) {\r\n return;\r\n }\r\n\r\n for (const card of findPatternCards(root)) {\r\n const index = card.dataset['patternIndex'] ?? '';\r\n const state: PatternState =\r\n session.request?.index === index ? session.state : 'idle';\r\n const label = readPatternCard(card, session.uid).label;\r\n\r\n if (state === 'idle') {\r\n card.removeAttribute(CARD_STATE_ATTRIBUTE);\r\n } else {\r\n card.setAttribute(CARD_STATE_ATTRIBUTE, state);\r\n }\r\n if (state !== 'playing') {\r\n clearSteps(card);\r\n }\r\n\r\n const button = card.querySelector<HTMLElement>(PLAY_BUTTON_SELECTOR);\r\n if (button) {\r\n this.#paintButton(button, state, label);\r\n }\r\n if (session.popupButton?.index === index) {\r\n this.#paintButton(session.popupButton.button, state, label);\r\n }\r\n }\r\n }\r\n\r\n #paintButton(button: HTMLElement, state: PatternState, label: string) {\r\n const text =\r\n state === 'idle'\r\n ? `Play ${label} on ${this.#instrumentLabel()}`\r\n : `Stop ${label}`;\r\n button.dataset['state'] = state;\r\n button.setAttribute('aria-pressed', String(state !== 'idle'));\r\n button.setAttribute('aria-label', text);\r\n button.title = text;\r\n }\r\n\r\n #instrumentLabel(): string {\r\n return getStrumInstrument(this.#instrument)?.label ?? this.#instrument;\r\n }\r\n\r\n /* ----------------------------- chord view ------------------------------ */\r\n\r\n /**\r\n * The chord popup's view toggle, beside its ▶: shows the chord on the\r\n * instrument in the popup's panel. It stays on for the next chord until\r\n * pressed again.\r\n */\r\n #createViewToggle(chord: string, panel: HTMLElement): HTMLButtonElement {\r\n const button = document.createElement('button');\r\n button.type = 'button';\r\n button.className = 'olpp-view-toggle';\r\n button.setAttribute(PLUGIN_UI_ATTRIBUTE, PLUGIN_ID);\r\n button.innerHTML = VIEW_ICON;\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n this.#chordViewOpen = !this.#chordViewOpen;\r\n this.#renderChordView();\r\n });\r\n this.#chordView = { chord, button, panel, shell: null };\r\n return button;\r\n }\r\n\r\n /** Mirror the toggle onto the popup open for one of this plugin's chords. */\r\n #renderChordView(): void {\r\n const view = this.#chordView;\r\n if (!view) {\r\n return;\r\n }\r\n if (!view.button.isConnected) {\r\n // The popup has moved on (or closed) since.\r\n this.#chordView = null;\r\n return;\r\n }\r\n const { chord, button, panel } = view;\r\n const text = this.#chordViewOpen\r\n ? `Back to the guitar diagram for ${chord}`\r\n : `Show ${chord} on ${this.#instrumentLabel()}`;\r\n button.setAttribute('aria-pressed', String(this.#chordViewOpen));\r\n button.setAttribute('aria-label', text);\r\n button.title = text;\r\n\r\n if (!this.#chordViewOpen) {\r\n view.shell = null;\r\n panel.replaceChildren();\r\n panel.hidden = true;\r\n return;\r\n }\r\n\r\n const card = this.player.resolveChord(this.#instrument, chord);\r\n if (card) {\r\n this.#drawChordView(card, view);\r\n return;\r\n }\r\n\r\n // The chord tables load on first use.\r\n view.shell = null;\r\n panel.replaceChildren(this.#createViewMessage('Loading…'));\r\n panel.hidden = false;\r\n this.player.loadChords().then(\r\n () => {\r\n if (this.#chordView === view) {\r\n this.#renderChordView();\r\n }\r\n },\r\n (error: unknown) => {\r\n if (this.#chordView === view) {\r\n panel.replaceChildren(\r\n this.#createViewMessage('Unable to load the chord voicings.'),\r\n );\r\n }\r\n console.error('Open Lyric player could not load its chords.', error);\r\n },\r\n );\r\n }\r\n\r\n /** The view's frame: the instrument between its ‹ › arrows, then the figure. */\r\n #createViewShell(): NonNullable<ChordView['shell']> {\r\n const view = document.createElement('div');\r\n view.className = 'olpp-view';\r\n\r\n const switcher = document.createElement('div');\r\n switcher.className = 'olpp-view__switcher';\r\n\r\n const caption = document.createElement('p');\r\n caption.className = 'olpp-view__caption';\r\n caption.setAttribute('aria-live', 'polite');\r\n\r\n const arrow = (step: -1 | 1) => {\r\n const button = document.createElement('button');\r\n button.type = 'button';\r\n button.className = `olpp-view__arrow olpp-view__arrow--${step < 0 ? 'previous' : 'next'}`;\r\n button.setAttribute(PLUGIN_UI_ATTRIBUTE, PLUGIN_ID);\r\n button.setAttribute(\r\n 'aria-label',\r\n step < 0 ? 'Previous instrument' : 'Next instrument',\r\n );\r\n button.title = button.getAttribute('aria-label')!;\r\n button.innerHTML = `<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"${step < 0 ? 'M15 5l-7 7 7 7' : 'M9 5l7 7-7 7'}\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`;\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n this.#stepInstrument(step);\r\n });\r\n return button;\r\n };\r\n\r\n const figure = document.createElement('div');\r\n figure.className = 'olpp-view__figure';\r\n\r\n const notes = document.createElement('p');\r\n notes.className = 'olpp-view__notes';\r\n\r\n switcher.append(arrow(-1), caption, arrow(1));\r\n view.append(switcher, figure, notes);\r\n return { view, caption, figure, notes };\r\n }\r\n\r\n /** Switch to the instrument before or after this one, wrapping around. */\r\n #stepInstrument(step: -1 | 1): void {\r\n const ids = OpenLyricPlayer.instruments.map((instrument) => instrument.id);\r\n const index = ids.indexOf(this.#instrument);\r\n this.instrument = ids[(index + step + ids.length) % ids.length]!;\r\n }\r\n\r\n #drawChordView(card: ChordCard, chordView: ChordView): void {\r\n const { panel } = chordView;\r\n const instrument = this.#instrumentLabel();\r\n const chord = card.chord;\r\n const midi = getChordCardMidi(card);\r\n\r\n if (!chordView.shell?.view.isConnected) {\r\n chordView.shell = this.#createViewShell();\r\n panel.replaceChildren(chordView.shell.view);\r\n }\r\n const { view, caption, figure, notes } = chordView.shell;\r\n view.dataset['kind'] = card.kind;\r\n caption.textContent = instrument;\r\n figure.replaceChildren();\r\n notes.textContent = midi\r\n .map((note) => getMidiNoteName(note, chord).replace(/-?\\d+$/, ''))\r\n .join(' ');\r\n notes.hidden = !midi.length;\r\n\r\n // Shown before drawing: SVGuitar measures its text as it draws.\r\n panel.hidden = false;\r\n\r\n if (card.kind === 'piano') {\r\n figure.append(createPianoBoard(chord, card.piano, chord));\r\n return;\r\n }\r\n if (card.kind === 'none') {\r\n figure.append(\r\n this.#createViewMessage(\r\n card.reason === 'not-a-chord'\r\n ? `“${chord}” can’t be read as a chord.`\r\n : `No ${instrument} voicing for ${chord}.`,\r\n ),\r\n );\r\n return;\r\n }\r\n\r\n // The popup's own diagram colours, read where the popup's theme applies.\r\n const token = (name: string, fallback: string) =>\r\n getComputedStyle(panel).getPropertyValue(name).trim() || fallback;\r\n try {\r\n new SVGuitarChord(figure)\r\n .configure({\r\n ...card.chart.settings,\r\n title: '',\r\n titleBottomMargin: 0,\r\n backgroundColor: 'transparent',\r\n color: token('--chord-popup-diagram-ink', '#213547'),\r\n fretColor: token('--chord-popup-diagram-line', '#52606d'),\r\n stringColor: token('--chord-popup-diagram-line', '#52606d'),\r\n fretLabelColor: token('--chord-popup-diagram-label', '#52606d'),\r\n tuningsColor: token('--chord-popup-diagram-label', '#52606d'),\r\n fingerColor: token('--accent', '#115e59'),\r\n fingerStrokeColor: token('--accent', '#115e59'),\r\n fingerTextColor: token('--surface', '#ffffff'),\r\n svgTitle: `${instrument} diagram for ${chord}`,\r\n })\r\n .chord(card.chart.chord)\r\n .draw();\r\n } catch (error) {\r\n figure.replaceChildren(\r\n this.#createViewMessage('Unable to draw the diagram.'),\r\n );\r\n console.error(`Open Lyric player could not draw ${chord}.`, error);\r\n }\r\n }\r\n\r\n #createViewMessage(text: string): HTMLElement {\r\n const message = document.createElement('p');\r\n message.className = 'olpp-view__message';\r\n message.textContent = text;\r\n return message;\r\n }\r\n\r\n /* ------------------------------- settings ------------------------------ */\r\n\r\n #renderInstrumentField(mount: HTMLElement): () => void {\r\n fieldCount += 1;\r\n const id = `olpp-instrument-${fieldCount}`;\r\n\r\n const label = document.createElement('label');\r\n label.className = 'topbar-settings__label';\r\n label.htmlFor = id;\r\n label.textContent = 'Instrument';\r\n\r\n const select = document.createElement('select');\r\n select.id = id;\r\n select.className = 'topbar-settings__text-input';\r\n for (const instrument of OpenLyricPlayer.instruments) {\r\n select.append(new Option(instrument.label, instrument.id));\r\n }\r\n select.value = this.#instrument;\r\n\r\n const note = document.createElement('p');\r\n note.className = 'topbar-settings__field-note';\r\n note.textContent = 'Plays a chord you click and a strumming pattern’s ▶.';\r\n\r\n const onChange = () => {\r\n if (isInstrumentId(select.value)) {\r\n this.instrument = select.value;\r\n }\r\n };\r\n select.addEventListener('change', onChange);\r\n this.#instrumentSelects.add(select);\r\n mount.append(label, select, note);\r\n\r\n return () => {\r\n select.removeEventListener('change', onChange);\r\n this.#instrumentSelects.delete(select);\r\n };\r\n }\r\n\r\n #readStoredInstrument(): StrumInstrumentId | null {\r\n if (!this.#storageKey) {\r\n return null;\r\n }\r\n try {\r\n const stored = globalThis.localStorage?.getItem(this.#storageKey);\r\n return isInstrumentId(stored) ? stored : null;\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n #writeStoredInstrument(instrument: StrumInstrumentId | null): void {\r\n if (!this.#storageKey) {\r\n return;\r\n }\r\n try {\r\n if (instrument) {\r\n globalThis.localStorage?.setItem(this.#storageKey, instrument);\r\n } else {\r\n globalThis.localStorage?.removeItem(this.#storageKey);\r\n }\r\n } catch {\r\n // Storage unavailable (private mode, a sandbox): the pick lasts the page.\r\n }\r\n }\r\n}\r\n","import { SVGuitarChord } from 'svguitar';\r\n\r\nimport {\r\n getChordCardMidi,\r\n getOpenLyricChordGroups,\r\n type ChordCard,\r\n} from './chord-library.ts';\r\nimport { createPianoBoard } from './chord-figure.ts';\r\nimport { getStrumSound } from './instrument-sounds.ts';\r\nimport { getMidiNoteName, type StrumInstrument } from './instruments.ts';\r\nimport { getChordRollSeconds } from './strum-plan.ts';\r\nimport type { OpenLyricPlayer } from './OpenLyricPlayer.ts';\r\nimport { START_DELAY_SECONDS } from './StrumPlayer.ts';\r\n\r\n/**\r\n * The playground's Chords section: every chord Open Lyric offers. Point at a\r\n * chord (or focus it) and a card shows its voicing on the selected instrument\r\n * — a chord diagram, or piano keys — with a play button that rolls the chord.\r\n * Pressing the chord itself plays it too.\r\n *\r\n * The card stays up while the pointer is on it and goes shortly after the\r\n * pointer leaves.\r\n */\r\n\r\nexport interface ChordLibraryOptions {\r\n /** The playground root: the card mounts here, inside its theme tokens. */\r\n root: HTMLElement;\r\n groups: HTMLElement;\r\n summary: HTMLElement;\r\n /** Unique within the page — the card's id, for `aria-describedby`. */\r\n cardId: string;\r\n player: OpenLyricPlayer;\r\n /** Aborted when the playground goes; drops the document listeners. */\r\n signal: AbortSignal;\r\n getInstrument(): StrumInstrument;\r\n getKey(): string;\r\n onError(message: string): void;\r\n}\r\n\r\nexport interface ChordLibrary {\r\n /** Re-reads the instrument and key into the chords and any open card. */\r\n refresh(): void;\r\n}\r\n\r\nconst HIDE_DELAY_MS = 400;\r\nconst CARD_GAP_PX = 10;\r\nconst VIEWPORT_MARGIN_PX = 8;\r\n/** How long a chord stays lit after its last note starts. */\r\nconst PLAYING_TAIL_MS = 350;\r\n\r\nconst PLAY_ICON =\r\n '<svg class=\"icon-play\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M8.5 5.8v12.4a.8.8 0 0 0 1.2.7l9.6-6.2a.8.8 0 0 0 0-1.4L9.7 5.1a.8.8 0 0 0-1.2.7z\"/></svg><span class=\"spinner\" aria-hidden=\"true\"></span>';\r\n\r\nfunction readToken(element: Element, name: string, fallback: string) {\r\n return getComputedStyle(element).getPropertyValue(name).trim() || fallback;\r\n}\r\n\r\nexport function mountChordLibrary(options: ChordLibraryOptions): ChordLibrary {\r\n const { root, groups, summary, player, signal } = options;\r\n const chips = new Map<string, HTMLButtonElement>();\r\n const flashTimers = new Map<string, ReturnType<typeof setTimeout>>();\r\n const card = document.createElement('div');\r\n const play = document.createElement('button');\r\n const figure = document.createElement('div');\r\n const notes = document.createElement('p');\r\n const message = document.createElement('p');\r\n let activeChip: HTMLButtonElement | null = null;\r\n let hideTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n card.className = 'chord-popup';\r\n card.id = options.cardId;\r\n card.hidden = true;\r\n card.setAttribute('role', 'group');\r\n play.className = 'chord-popup__play';\r\n play.type = 'button';\r\n play.innerHTML = PLAY_ICON;\r\n figure.className = 'chord-popup__figure';\r\n notes.className = 'chord-popup__notes';\r\n message.className = 'chord-popup__message';\r\n card.append(play, figure, notes, message);\r\n root.append(card);\r\n\r\n function resolve(chord: string): ChordCard {\r\n return (\r\n player.resolveChord(options.getInstrument().id, chord) ?? {\r\n kind: 'none',\r\n chord,\r\n reason: 'no-voicing',\r\n }\r\n );\r\n }\r\n\r\n function describeMissing(popup: Extract<ChordCard, { kind: 'none' }>) {\r\n const instrument = options.getInstrument().label;\r\n\r\n return popup.reason === 'not-a-chord'\r\n ? `Open Lyric's chord theory can't read “${popup.chord}” as a chord, so there is nothing to play.`\r\n : `No playable ${instrument} voicing was found for ${popup.chord}.`;\r\n }\r\n\r\n // ——— Chords ———\r\n\r\n function renderGroups() {\r\n groups.replaceChildren(\r\n ...getOpenLyricChordGroups().map((group) => {\r\n const section = document.createElement('div');\r\n const name = document.createElement('h3');\r\n const list = document.createElement('div');\r\n\r\n section.className = 'chord-group';\r\n name.className = 'chord-group__name';\r\n name.textContent = group.name;\r\n list.className = 'chord-group__chords';\r\n list.setAttribute('role', 'group');\r\n list.setAttribute('aria-label', group.name);\r\n\r\n for (const chord of group.chords) {\r\n const chip = document.createElement('button');\r\n\r\n chip.type = 'button';\r\n chip.className = 'chord-chip';\r\n chip.dataset.chord = chord;\r\n chip.textContent = chord;\r\n chips.set(chord, chip);\r\n list.append(chip);\r\n }\r\n\r\n section.append(name, list);\r\n return section;\r\n }),\r\n );\r\n }\r\n\r\n function refreshChips() {\r\n const instrument = options.getInstrument();\r\n let playable = 0;\r\n\r\n for (const [chord, chip] of chips) {\r\n const available = getChordCardMidi(resolve(chord)).length > 0;\r\n\r\n playable += Number(available);\r\n chip.dataset.available = String(available);\r\n chip.setAttribute(\r\n 'aria-label',\r\n available\r\n ? `${chord}, play on ${instrument.label}`\r\n : `${chord}, nothing to play on ${instrument.label}`,\r\n );\r\n }\r\n\r\n summary.textContent = `${playable} of ${chips.size} chords have a voicing on ${instrument.label}.`;\r\n }\r\n\r\n // ——— The card ———\r\n\r\n function renderCard(chord: string) {\r\n const popup = resolve(chord);\r\n const midi = getChordCardMidi(popup);\r\n const instrument = options.getInstrument();\r\n const keyText = options.getKey();\r\n\r\n card.dataset.kind = popup.kind;\r\n card.setAttribute('aria-label', `${chord} on ${instrument.label}`);\r\n figure.replaceChildren();\r\n message.textContent = '';\r\n\r\n if (popup.kind === 'diagram') {\r\n new SVGuitarChord(figure)\r\n .configure({\r\n ...popup.chart.settings,\r\n color: readToken(card, '--diagram-ink', '#1f2328'),\r\n fingerColor: readToken(card, '--diagram-dot', '#3f4650'),\r\n fingerStrokeColor: readToken(card, '--diagram-dot', '#3f4650'),\r\n fingerTextColor: readToken(card, '--diagram-dot-text', '#ffffff'),\r\n svgTitle: `${instrument.label} diagram for ${chord}`,\r\n })\r\n .chord(popup.chart.chord)\r\n .draw();\r\n\r\n if (popup.voicing.source === 'computed') {\r\n message.textContent = 'Voicing computed from the chord’s notes.';\r\n }\r\n } else {\r\n const title = document.createElement('p');\r\n\r\n title.className = 'chord-popup__title';\r\n title.textContent = chord;\r\n figure.append(title);\r\n\r\n if (popup.kind === 'piano') {\r\n figure.append(createPianoBoard(chord, popup.piano, keyText));\r\n } else {\r\n message.textContent = describeMissing(popup);\r\n }\r\n }\r\n\r\n notes.textContent = midi\r\n .map((note) => getMidiNoteName(note, keyText))\r\n .join(' ');\r\n notes.hidden = !midi.length;\r\n message.hidden = !message.textContent;\r\n play.disabled = !midi.length;\r\n play.setAttribute(\r\n 'aria-label',\r\n midi.length\r\n ? `Play ${chord} on ${instrument.label}`\r\n : `Nothing to play for ${chord}`,\r\n );\r\n play.title = play.getAttribute('aria-label')!;\r\n syncPlayButton();\r\n }\r\n\r\n function syncPlayButton() {\r\n play.dataset.state = (activeChip && activeChip.dataset.state) || '';\r\n }\r\n\r\n function placeCard(chip: HTMLElement) {\r\n const anchor = chip.getBoundingClientRect();\r\n const { offsetWidth: width, offsetHeight: height } = card;\r\n let top = anchor.top - height - CARD_GAP_PX;\r\n\r\n if (top < VIEWPORT_MARGIN_PX) {\r\n top = anchor.bottom + CARD_GAP_PX;\r\n }\r\n\r\n const left = Math.max(\r\n VIEWPORT_MARGIN_PX,\r\n Math.min(\r\n anchor.left + anchor.width / 2 - width / 2,\r\n window.innerWidth - width - VIEWPORT_MARGIN_PX,\r\n ),\r\n );\r\n\r\n card.style.top = `${Math.round(top)}px`;\r\n card.style.left = `${Math.round(left)}px`;\r\n }\r\n\r\n function cancelHide() {\r\n if (hideTimer != null) {\r\n clearTimeout(hideTimer);\r\n hideTimer = null;\r\n }\r\n }\r\n\r\n function showCard(chip: HTMLButtonElement) {\r\n cancelHide();\r\n\r\n if (chip !== activeChip || card.hidden) {\r\n activeChip?.classList.remove('is-open');\r\n activeChip?.removeAttribute('aria-describedby');\r\n activeChip = chip;\r\n chip.classList.add('is-open');\r\n chip.setAttribute('aria-describedby', card.id);\r\n // Shown first: SVGuitar measures its title as it draws, and text in a\r\n // hidden card measures zero.\r\n card.hidden = false;\r\n renderCard(chip.dataset.chord!);\r\n }\r\n\r\n placeCard(chip);\r\n }\r\n\r\n function hideCard() {\r\n cancelHide();\r\n card.hidden = true;\r\n activeChip?.classList.remove('is-open');\r\n activeChip?.removeAttribute('aria-describedby');\r\n activeChip = null;\r\n }\r\n\r\n function scheduleHide() {\r\n cancelHide();\r\n hideTimer = setTimeout(hideCard, HIDE_DELAY_MS);\r\n }\r\n\r\n // ——— Playing ———\r\n\r\n function setChipState(chord: string, state: '' | 'loading' | 'playing') {\r\n const chip = chips.get(chord);\r\n\r\n if (chip) {\r\n chip.dataset.state = state;\r\n }\r\n\r\n syncPlayButton();\r\n }\r\n\r\n function flash(\r\n chord: string,\r\n noteCount: number,\r\n instrument: StrumInstrument,\r\n ) {\r\n clearTimeout(flashTimers.get(chord));\r\n setChipState(chord, 'playing');\r\n flashTimers.set(\r\n chord,\r\n setTimeout(\r\n () => {\r\n flashTimers.delete(chord);\r\n setChipState(chord, '');\r\n },\r\n (START_DELAY_SECONDS +\r\n getChordRollSeconds(getStrumSound(instrument.id), noteCount)) *\r\n 1000 +\r\n PLAYING_TAIL_MS,\r\n ),\r\n );\r\n }\r\n\r\n async function playChord(chord: string) {\r\n const midi = getChordCardMidi(resolve(chord));\r\n\r\n if (!midi.length) {\r\n return;\r\n }\r\n\r\n const instrument = options.getInstrument();\r\n\r\n setChipState(chord, 'loading');\r\n\r\n try {\r\n const played = await player.playChord(chord, instrument.id);\r\n\r\n if (played) {\r\n flash(chord, midi.length, instrument);\r\n } else {\r\n setChipState(chord, '');\r\n }\r\n } catch (error) {\r\n setChipState(chord, '');\r\n options.onError(\r\n `Could not play ${chord}: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n }\r\n\r\n // ——— Wiring ———\r\n\r\n function chipFrom(target: EventTarget | null): HTMLButtonElement | null {\r\n return target instanceof Element\r\n ? target.closest<HTMLButtonElement>('.chord-chip')\r\n : null;\r\n }\r\n\r\n groups.addEventListener('pointerover', (event) => {\r\n const chip = chipFrom(event.target);\r\n\r\n if (chip) {\r\n showCard(chip);\r\n }\r\n });\r\n\r\n groups.addEventListener('pointerout', (event) => {\r\n const chip = chipFrom(event.target);\r\n const next = event.relatedTarget;\r\n\r\n if (\r\n chip &&\r\n !(next instanceof Node && (chip.contains(next) || card.contains(next)))\r\n ) {\r\n scheduleHide();\r\n }\r\n });\r\n\r\n groups.addEventListener('focusin', (event) => {\r\n const chip = chipFrom(event.target);\r\n\r\n if (chip) {\r\n showCard(chip);\r\n }\r\n });\r\n\r\n groups.addEventListener('focusout', (event) => {\r\n const next = event.relatedTarget;\r\n\r\n if (!(next instanceof Node && card.contains(next)) && !chipFrom(next)) {\r\n scheduleHide();\r\n }\r\n });\r\n\r\n groups.addEventListener('click', (event) => {\r\n const chip = chipFrom(event.target);\r\n\r\n if (chip) {\r\n showCard(chip);\r\n void playChord(chip.dataset.chord!);\r\n }\r\n });\r\n\r\n card.addEventListener('pointerenter', cancelHide);\r\n card.addEventListener('pointerleave', scheduleHide);\r\n play.addEventListener('click', () => {\r\n if (activeChip) {\r\n void playChord(activeChip.dataset.chord!);\r\n }\r\n });\r\n\r\n document.addEventListener(\r\n 'keydown',\r\n (event) => {\r\n if (event.key === 'Escape' && !card.hidden) {\r\n hideCard();\r\n }\r\n },\r\n { signal },\r\n );\r\n\r\n document.addEventListener(\r\n 'pointerdown',\r\n (event) => {\r\n const target = event.target;\r\n\r\n if (\r\n !card.hidden &&\r\n !(target instanceof Node && card.contains(target)) &&\r\n !chipFrom(target)\r\n ) {\r\n hideCard();\r\n }\r\n },\r\n { signal },\r\n );\r\n\r\n const follow = () => {\r\n if (activeChip && !card.hidden) {\r\n placeCard(activeChip);\r\n }\r\n };\r\n\r\n window.addEventListener('scroll', follow, {\r\n passive: true,\r\n capture: true,\r\n signal,\r\n });\r\n window.addEventListener('resize', follow, { signal });\r\n signal.addEventListener('abort', () => {\r\n cancelHide();\r\n flashTimers.forEach((timer) => clearTimeout(timer));\r\n flashTimers.clear();\r\n });\r\n\r\n renderGroups();\r\n refreshChips();\r\n\r\n return {\r\n refresh() {\r\n refreshChips();\r\n\r\n if (activeChip && !card.hidden) {\r\n renderCard(activeChip.dataset.chord!);\r\n placeCard(activeChip);\r\n }\r\n },\r\n };\r\n}\r\n","/**\r\n * The playground's markup. Every element the code reaches carries a\r\n * `data-olp-ref`; ids exist only for label/ARIA links and are prefixed per\r\n * instance, so two playgrounds can share a page.\r\n */\r\nexport function createPlaygroundMarkup(prefix: string): string {\r\n const id = (name: string) => `${prefix}-${name}`;\r\n\r\n return `\r\n <div class=\"page\">\r\n <header class=\"page-header\">\r\n <div>\r\n <h1>Strum Player</h1>\r\n <p>\r\n Hear an Open Lyric song's strumming patterns — and every chord Open\r\n Lyric offers — on guitars, keyboards, ukuleles and basses. Patterns\r\n are written in the notation's Strumming Patterns syntax.\r\n </p>\r\n </div>\r\n </header>\r\n\r\n <section class=\"panel song-bar\" aria-label=\"Song\">\r\n <label class=\"song-field\">Key <select data-olp-ref=\"key\"></select></label>\r\n <label class=\"song-field\">Time <select data-olp-ref=\"time\"></select></label>\r\n <div class=\"song-field song-field--tempo\">\r\n <label for=\"${id('tempo')}\">Tempo</label>\r\n <input data-olp-ref=\"tempoRange\" type=\"range\" min=\"20\" max=\"300\" step=\"1\" aria-label=\"Tempo slider\" />\r\n <input data-olp-ref=\"tempo\" id=\"${id('tempo')}\" type=\"number\" min=\"20\" max=\"300\" step=\"1\" inputmode=\"numeric\" />\r\n <span>bpm</span>\r\n </div>\r\n </section>\r\n\r\n <div class=\"layout\">\r\n <section class=\"panel instruments\" aria-labelledby=\"${id('instruments')}\">\r\n <h2 id=\"${id('instruments')}\">Instruments</h2>\r\n <div data-olp-ref=\"instruments\" class=\"instrument-list\" role=\"radiogroup\" aria-labelledby=\"${id('instruments')}\"></div>\r\n <div data-olp-ref=\"voicing\" class=\"voicing\" aria-live=\"polite\"></div>\r\n </section>\r\n\r\n <section class=\"panel patterns\" aria-labelledby=\"${id('patterns')}\">\r\n <div class=\"patterns__header\">\r\n <h2 id=\"${id('patterns')}\">Patterns</h2>\r\n <button data-olp-ref=\"restore\" class=\"text-button\" type=\"button\">Restore example patterns</button>\r\n </div>\r\n <div data-olp-ref=\"patterns\" class=\"pattern-list\"></div>\r\n <p data-olp-ref=\"patternsEmpty\" class=\"pattern-list__empty\" hidden>No patterns — add one below.</p>\r\n <details class=\"add-patterns\">\r\n <summary>Add patterns</summary>\r\n <p>\r\n Open Lyric notation, one pattern per line — <code>d</code> down,\r\n <code>u</code> up, <code>x</code> alternating, <code>-</code> rest,\r\n <code>|</code> between groups, and a <code>// label</code> line\r\n above to name it. Each character is one sixteenth note.\r\n </p>\r\n <label class=\"visually-hidden\" for=\"${id('input')}\">Patterns to add</label>\r\n <textarea data-olp-ref=\"input\" id=\"${id('input')}\" spellcheck=\"false\" placeholder=\"// Island strum&#10;d-d-u-u-d-u-d-u-\"></textarea>\r\n <div class=\"add-patterns__actions\">\r\n <button data-olp-ref=\"add\" class=\"primary-button\" type=\"button\">Add</button>\r\n <span data-olp-ref=\"addError\" class=\"add-patterns__error\" role=\"alert\"></span>\r\n </div>\r\n </details>\r\n </section>\r\n </div>\r\n\r\n <p data-olp-ref=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\"></p>\r\n\r\n <section class=\"panel chords\" aria-labelledby=\"${id('chords')}\">\r\n <div class=\"chords__header\">\r\n <h2 id=\"${id('chords')}\">Chords</h2>\r\n <p data-olp-ref=\"chordsSummary\" class=\"chords__summary\"></p>\r\n </div>\r\n <p class=\"chords__intro\">\r\n Every chord Open Lyric offers. Point at one for its voicing on the\r\n selected instrument, then press the play button on the card, or the\r\n chord itself, to hear it rolled from the lowest string up.\r\n </p>\r\n <div data-olp-ref=\"chords\" class=\"chord-groups\"></div>\r\n </section>\r\n\r\n <details class=\"panel notes\">\r\n <summary>How it plays</summary>\r\n <ul>\r\n <li>\r\n Sound: FluidR3 soundfonts, served with this player and heard in a\r\n small room — nylon guitar for the classical guitar and both\r\n ukuleles (General MIDI has no ukulele), and an electric bass for\r\n Bass, whose line carries on small speakers; the Upright Bass is\r\n darker, as the instrument is.\r\n </li>\r\n <li>\r\n Notes: a pattern strums the song key's chord as the instrument\r\n voices it, in standard tuning — the same voicing the chord's card\r\n shows below.\r\n </li>\r\n <li>\r\n Voicings: guitar and ukulele shapes come from the chords-db\r\n database the editor's chord popup uses, or are computed from the\r\n chord's notes when a computed shape is clearly easier; baritone\r\n ukulele shapes are computed. On guitar and baritone the lowest note\r\n is always the chord's bass. Bass plays the root with its fifth and\r\n octave; piano stacks the chord from its root in octave 3.\r\n </li>\r\n <li>\r\n Strokes: each character of a pattern is a sixteenth note at the\r\n song's tempo. Guitar and ukuleles strum in string order — down from\r\n the lowest string, up from the highest, catching only the top\r\n strings. The keyboards play the chord, the right hand alone on an\r\n up stroke, and the organ holds it; the basses pluck the root on a\r\n down stroke and the octave on an up stroke. A string rings until it is struck again; stopping a\r\n pattern damps the strings still ringing.\r\n </li>\r\n <li>\r\n Feel: the bar's first beat is loudest, then the other beats, the\r\n off-beats, and the sixteenths in between. A soft stroke sounds\r\n darker than a hard one, and each lands a little differently, as a\r\n player's would.\r\n </li>\r\n <li>\r\n Chord cards roll the chord string by string, and bass plucks each\r\n note in turn. A name Open Lyric's chord theory can't read (Cmaj2,\r\n Am4, …) has nothing to play.\r\n </li>\r\n </ul>\r\n </details>\r\n </div>\r\n `;\r\n}\r\n","import playgroundCss from './OpenLyricPlayerPlayground.scss?inline';\r\nimport {\r\n CONFIG_ENUM_FIELD_VALUES,\r\n CONFIG_KEY_VALUES,\r\n} from '../../editor/scripts/shared.ts';\r\nimport {\r\n STRUM_INSTRUMENTS,\r\n getMidiNoteName,\r\n getStrumInstrument,\r\n type StrumInstrument,\r\n type StrumInstrumentId,\r\n type StrumVoicing,\r\n} from './instruments.ts';\r\nimport {\r\n OpenLyricPlayer,\r\n type OpenLyricPlayerStateEvent,\r\n} from './OpenLyricPlayer.ts';\r\nimport {\r\n cycleStrumPatternStep,\r\n parseOpenLyricPattern,\r\n readStrumPatternInput,\r\n type StrumPattern,\r\n type StrumPatternDefinition,\r\n type StrumStep,\r\n} from './patterns.ts';\r\nimport { mountChordLibrary, type ChordLibrary } from './playground-chords.ts';\r\nimport { createPlaygroundMarkup } from './playground-markup.ts';\r\n\r\n/**\r\n * `OpenLyricPlayerPlayground` — hear an Open Lyric song's strumming patterns,\r\n * and every chord Open Lyric offers, on guitars, keyboards, ukuleles and\r\n * basses. Patterns are written in the notation's `Strumming Patterns`\r\n * syntax; it opens on the two patterns of the \"Extended Worship Build\" example\r\n * (Key E, 4/4, 70 bpm). Below them, every chord gets a card with its voicing\r\n * and a play button (`playground-chords.ts`).\r\n *\r\n * The UI over an {@link OpenLyricPlayer}. It renders into a root element of\r\n * its own inside `container`, and its styles are scoped to that root.\r\n *\r\n * Settings and patterns persist in localStorage under `storageKey` — a\r\n * convenience only; it starts from the example whenever nothing usable is\r\n * stored.\r\n */\r\n\r\nexport type OpenLyricPlayerTheme = 'light' | 'dark';\r\n\r\nexport interface OpenLyricPlayerPlaygroundOptions {\r\n /** Where the playground renders. Its existing content is left alone. */\r\n container: HTMLElement;\r\n /** A player to share with the host; otherwise the playground makes one. */\r\n player?: OpenLyricPlayer;\r\n /** `light` or `dark`; omitted, the system's colour scheme decides. */\r\n theme?: OpenLyricPlayerTheme;\r\n /**\r\n * localStorage key for the settings and patterns. Default\r\n * `openLyricStrumPlayer.v2`; `null` keeps nothing between visits.\r\n */\r\n storageKey?: string | null;\r\n}\r\n\r\ninterface PlaySettings {\r\n key: string;\r\n time: string;\r\n tempo: number;\r\n instrument: StrumInstrumentId;\r\n patterns: StrumPatternDefinition[];\r\n}\r\n\r\ntype Refs = {\r\n key: HTMLSelectElement;\r\n time: HTMLSelectElement;\r\n tempo: HTMLInputElement;\r\n tempoRange: HTMLInputElement;\r\n instruments: HTMLElement;\r\n voicing: HTMLElement;\r\n patterns: HTMLElement;\r\n patternsEmpty: HTMLElement;\r\n restore: HTMLButtonElement;\r\n input: HTMLTextAreaElement;\r\n add: HTMLButtonElement;\r\n addError: HTMLElement;\r\n status: HTMLElement;\r\n chords: HTMLElement;\r\n chordsSummary: HTMLElement;\r\n};\r\n\r\nconst REF_NAMES: readonly (keyof Refs)[] = [\r\n 'key',\r\n 'time',\r\n 'tempo',\r\n 'tempoRange',\r\n 'instruments',\r\n 'voicing',\r\n 'patterns',\r\n 'patternsEmpty',\r\n 'restore',\r\n 'input',\r\n 'add',\r\n 'addError',\r\n 'status',\r\n 'chords',\r\n 'chordsSummary',\r\n];\r\n\r\nconst DEFAULT_STORAGE_KEY = 'openLyricStrumPlayer.v2';\r\nconst TEMPO_MIN = 20;\r\nconst TEMPO_MAX = 300;\r\nconst KEYS: readonly string[] = CONFIG_KEY_VALUES;\r\nconst TIME_SIGNATURES: readonly string[] = CONFIG_ENUM_FIELD_VALUES.Time;\r\nconst THEMES: readonly OpenLyricPlayerTheme[] = ['light', 'dark'];\r\n\r\n/** The `Strumming Patterns` of `examples/example1.md`. */\r\nconst EXAMPLE_PATTERNS: readonly StrumPatternDefinition[] = [\r\n { id: 'example-1', label: 'Slow rolling pulse', source: 'd--u-- | d-u-d-' },\r\n { id: 'example-2', label: 'Lift on the chorus', source: 'ddu--u | d-d-du' },\r\n];\r\n\r\nconst DEFAULT_SETTINGS: Omit<PlaySettings, 'patterns'> = {\r\n key: 'E',\r\n time: '4/4',\r\n tempo: 70,\r\n instrument: 'guitar',\r\n};\r\n\r\nconst ARROW_SVG =\r\n '<svg viewBox=\"0 0 14 28\" aria-hidden=\"true\"><path d=\"M7 3v21M2.5 19.5 7 24.5l4.5-5\"/></svg>';\r\n\r\nlet instanceCount = 0;\r\n\r\n// ——— The stylesheet: one <style> per page, however many playgrounds ———\r\n//\r\n// Inline (`?inline`), not a <link> to the emitted .css: a Vite dev host serves\r\n// a `.css` under `node_modules` as a JavaScript HMR module, and a browser\r\n// refuses a stylesheet that is not `text/css`. See `OpenLyricPluginPlayer.ts`.\r\n\r\nlet stylesheetUsers = 0;\r\nlet stylesheet: HTMLStyleElement | null = null;\r\n\r\nfunction acquireStylesheet() {\r\n stylesheetUsers += 1;\r\n\r\n if (!stylesheet) {\r\n stylesheet = document.createElement('style');\r\n stylesheet.textContent = playgroundCss;\r\n stylesheet.dataset.olpStylesheet = '';\r\n document.head.append(stylesheet);\r\n }\r\n}\r\n\r\nfunction releaseStylesheet() {\r\n stylesheetUsers = Math.max(0, stylesheetUsers - 1);\r\n\r\n if (!stylesheetUsers) {\r\n stylesheet?.remove();\r\n stylesheet = null;\r\n }\r\n}\r\n\r\n// ——— Settings ———\r\n\r\nfunction copyExamplePatterns(): StrumPatternDefinition[] {\r\n return EXAMPLE_PATTERNS.map((definition) => ({ ...definition }));\r\n}\r\n\r\nfunction clampTempo(value: unknown): number | null {\r\n const tempo = Math.round(Number(value));\r\n\r\n return Number.isFinite(tempo)\r\n ? Math.min(TEMPO_MAX, Math.max(TEMPO_MIN, tempo))\r\n : null;\r\n}\r\n\r\nfunction isUsableDefinition(value: any): value is StrumPatternDefinition {\r\n if (\r\n !value ||\r\n typeof value.id !== 'string' ||\r\n typeof value.label !== 'string' ||\r\n typeof value.source !== 'string'\r\n ) {\r\n return false;\r\n }\r\n\r\n try {\r\n parseOpenLyricPattern(value, '4/4');\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nfunction readSettings(storageKey: string | null): PlaySettings {\r\n let stored: any = null;\r\n\r\n try {\r\n stored = storageKey\r\n ? JSON.parse(localStorage.getItem(storageKey) ?? 'null')\r\n : null;\r\n } catch {\r\n stored = null;\r\n }\r\n\r\n if (!stored || typeof stored !== 'object') {\r\n return { ...DEFAULT_SETTINGS, patterns: copyExamplePatterns() };\r\n }\r\n\r\n const storedPatterns = Array.isArray(stored.patterns)\r\n ? stored.patterns\r\n .filter(isUsableDefinition)\r\n .map(({ id, label, source }: StrumPatternDefinition) => ({\r\n id,\r\n label,\r\n source,\r\n }))\r\n : null;\r\n\r\n return {\r\n key: KEYS.includes(stored.key) ? stored.key : DEFAULT_SETTINGS.key,\r\n time: TIME_SIGNATURES.includes(stored.time)\r\n ? stored.time\r\n : DEFAULT_SETTINGS.time,\r\n tempo: clampTempo(stored.tempo) ?? DEFAULT_SETTINGS.tempo,\r\n instrument:\r\n getStrumInstrument(stored.instrument)?.id ?? DEFAULT_SETTINGS.instrument,\r\n patterns: storedPatterns ?? copyExamplePatterns(),\r\n };\r\n}\r\n\r\nfunction describeStep(step: StrumStep | null): string {\r\n return step ? step.direction : 'rest';\r\n}\r\n\r\nfunction createPlayIcon(): SVGSVGElement[] {\r\n const template = document.createElement('template');\r\n\r\n template.innerHTML =\r\n '<svg class=\"icon-play\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M8.5 5.8v12.4a.8.8 0 0 0 1.2.7l9.6-6.2a.8.8 0 0 0 0-1.4L9.7 5.1a.8.8 0 0 0-1.2.7z\"/></svg>' +\r\n '<svg class=\"icon-stop\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><rect x=\"6.5\" y=\"6.5\" width=\"11\" height=\"11\" rx=\"1.6\"/></svg>';\r\n\r\n return Array.from(template.content.children) as SVGSVGElement[];\r\n}\r\n\r\nfunction fillStep(element: HTMLElement, pattern: StrumPattern, index: number) {\r\n const step = pattern.steps[index];\r\n const count = pattern.counts[index] ?? String(index + 1);\r\n const description = `${count} (step ${index + 1}): ${describeStep(step)}`;\r\n const [stroke, countElement] = Array.from(element.children) as HTMLElement[];\r\n\r\n element.classList.toggle('is-active', Boolean(step));\r\n element.classList.toggle('step--break', pattern.groupBreaks.includes(index));\r\n element.setAttribute('aria-label', `${description}. Press to change.`);\r\n element.title = description;\r\n stroke.innerHTML = step ? ARROW_SVG : '';\r\n stroke.firstElementChild?.classList.toggle(\r\n 'stroke-up',\r\n step?.direction === 'up',\r\n );\r\n countElement.textContent = count;\r\n}\r\n\r\nfunction createStep(pattern: StrumPattern, index: number): HTMLElement {\r\n const element = document.createElement('button');\r\n\r\n element.type = 'button';\r\n element.className = 'step';\r\n element.dataset.step = String(index + 1);\r\n element.dataset.action = 'cycle';\r\n\r\n for (const part of ['stroke', 'count']) {\r\n const child = document.createElement('span');\r\n\r\n child.className = `step__${part}`;\r\n child.setAttribute('aria-hidden', 'true');\r\n element.append(child);\r\n }\r\n\r\n fillStep(element, pattern, index);\r\n return element;\r\n}\r\n\r\nfunction createPatternCard(pattern: StrumPattern): HTMLElement {\r\n const card = document.createElement('article');\r\n const header = document.createElement('div');\r\n const play = document.createElement('button');\r\n const spinner = document.createElement('span');\r\n const name = document.createElement('h3');\r\n const remove = document.createElement('button');\r\n const steps = document.createElement('div');\r\n const source = document.createElement('p');\r\n\r\n card.className = 'pattern-card';\r\n card.dataset.patternId = pattern.id;\r\n card.dataset.state = 'idle';\r\n header.className = 'pattern-card__header';\r\n play.className = 'pattern-card__play';\r\n play.type = 'button';\r\n play.dataset.action = 'toggle';\r\n spinner.className = 'spinner';\r\n spinner.setAttribute('aria-hidden', 'true');\r\n play.append(...createPlayIcon(), spinner);\r\n name.className = 'pattern-card__name';\r\n name.textContent = pattern.label;\r\n remove.className = 'pattern-card__remove';\r\n remove.type = 'button';\r\n remove.dataset.action = 'remove';\r\n remove.textContent = '×';\r\n remove.title = `Remove ${pattern.label}`;\r\n remove.setAttribute('aria-label', `Remove ${pattern.label}`);\r\n header.append(play, name, remove);\r\n\r\n steps.className = 'pattern-card__steps';\r\n steps.setAttribute('role', 'group');\r\n steps.setAttribute('aria-label', `${pattern.label} steps`);\r\n steps.append(...pattern.steps.map((_, index) => createStep(pattern, index)));\r\n\r\n source.className = 'pattern-card__source';\r\n source.append(\r\n Object.assign(document.createElement('code'), {\r\n textContent: pattern.source,\r\n }),\r\n );\r\n card.append(header, steps, source);\r\n\r\n return card;\r\n}\r\n\r\nexport class OpenLyricPlayerPlayground {\r\n /** The player behind the UI — the one passed in, or the playground's own. */\r\n readonly player: OpenLyricPlayer;\r\n\r\n #container: HTMLElement;\r\n #ownsPlayer: boolean;\r\n #storageKey: string | null;\r\n #theme: OpenLyricPlayerTheme | undefined;\r\n #prefix: string;\r\n #root: HTMLElement | null = null;\r\n #refs: Refs | null = null;\r\n #abort: AbortController | null = null;\r\n #unsubscribe: (() => void)[] = [];\r\n #settings: PlaySettings;\r\n #patterns = new Map<string, StrumPattern>();\r\n #currentStep: HTMLElement | null = null;\r\n #chordLibrary: ChordLibrary | null = null;\r\n #idCounter = 0;\r\n\r\n constructor(options: OpenLyricPlayerPlaygroundOptions) {\r\n if (!(options?.container instanceof HTMLElement)) {\r\n throw new TypeError(\r\n 'OpenLyricPlayerPlayground needs a `container` element.',\r\n );\r\n }\r\n\r\n this.#container = options.container;\r\n this.#ownsPlayer = !options.player;\r\n this.player = options.player ?? new OpenLyricPlayer();\r\n this.#storageKey =\r\n options.storageKey === undefined\r\n ? DEFAULT_STORAGE_KEY\r\n : options.storageKey;\r\n this.#theme = this.#checkTheme(options.theme);\r\n this.#prefix = `olp-${++instanceCount}`;\r\n this.#settings = readSettings(this.#storageKey);\r\n }\r\n\r\n /** The root element, once rendered. */\r\n get element(): HTMLElement | null {\r\n return this.#root;\r\n }\r\n\r\n /** `light`, `dark`, or `undefined` when the system decides. */\r\n get theme(): OpenLyricPlayerTheme | undefined {\r\n return this.#theme;\r\n }\r\n\r\n /** Switches the theme; `undefined` hands it back to the system. */\r\n setTheme(theme: OpenLyricPlayerTheme | undefined): void {\r\n this.#theme = this.#checkTheme(theme);\r\n this.#applyTheme();\r\n }\r\n\r\n /**\r\n * Renders into the container. Calling it again renders afresh, keeping the\r\n * settings and patterns.\r\n */\r\n render(): this {\r\n this.#teardown();\r\n acquireStylesheet();\r\n\r\n const root = document.createElement('div');\r\n const abort = new AbortController();\r\n\r\n root.className = 'olp-playground';\r\n root.innerHTML = createPlaygroundMarkup(this.#prefix);\r\n this.#root = root;\r\n this.#abort = abort;\r\n this.#refs = Object.fromEntries(\r\n REF_NAMES.map((name) => {\r\n const element = root.querySelector(`[data-olp-ref=\"${name}\"]`);\r\n\r\n if (!element) {\r\n throw new Error(`The playground markup is missing ${name}.`);\r\n }\r\n\r\n return [name, element];\r\n }),\r\n ) as Refs;\r\n this.#applyTheme();\r\n this.#container.append(root);\r\n\r\n this.#wire(abort.signal);\r\n this.#renderSongControls();\r\n this.#renderInstruments();\r\n this.#renderVoicing();\r\n this.#renderPatterns();\r\n this.#loadChords(abort.signal);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Removes the playground and stops its pattern. A player the playground\r\n * made is destroyed with it; one passed in is left to its owner.\r\n */\r\n destroy(): void {\r\n this.#teardown();\r\n\r\n if (this.#ownsPlayer) {\r\n this.player.destroy();\r\n }\r\n }\r\n\r\n #teardown() {\r\n if (!this.#root) {\r\n return;\r\n }\r\n\r\n if (this.#patterns.has(this.player.patternId ?? '')) {\r\n this.player.stop();\r\n }\r\n\r\n this.#abort?.abort();\r\n this.#unsubscribe.forEach((unsubscribe) => unsubscribe());\r\n this.#unsubscribe = [];\r\n this.#root.remove();\r\n this.#root = null;\r\n this.#refs = null;\r\n this.#abort = null;\r\n this.#chordLibrary = null;\r\n this.#currentStep = null;\r\n releaseStylesheet();\r\n }\r\n\r\n #checkTheme(theme: unknown): OpenLyricPlayerTheme | undefined {\r\n if (theme === undefined || theme === null) {\r\n return undefined;\r\n }\r\n\r\n if (!THEMES.includes(theme as OpenLyricPlayerTheme)) {\r\n throw new TypeError(\r\n `Unknown theme “${String(theme)}” — use 'light' or 'dark'.`,\r\n );\r\n }\r\n\r\n return theme as OpenLyricPlayerTheme;\r\n }\r\n\r\n #applyTheme() {\r\n if (!this.#root) {\r\n return;\r\n }\r\n\r\n if (this.#theme) {\r\n this.#root.dataset.theme = this.#theme;\r\n } else {\r\n delete this.#root.dataset.theme;\r\n }\r\n\r\n // SVGuitar reads the diagram inks once, as it draws.\r\n this.#chordLibrary?.refresh();\r\n }\r\n\r\n #saveSettings() {\r\n if (!this.#storageKey) {\r\n return;\r\n }\r\n\r\n try {\r\n localStorage.setItem(this.#storageKey, JSON.stringify(this.#settings));\r\n } catch {\r\n // Storage is a convenience; the playground works without it.\r\n }\r\n }\r\n\r\n #getInstrument(): StrumInstrument {\r\n return (\r\n getStrumInstrument(this.#settings.instrument) ?? STRUM_INSTRUMENTS[0]\r\n );\r\n }\r\n\r\n #getVoicing(): StrumVoicing | null {\r\n return this.player.getVoicing(\r\n this.#settings.instrument,\r\n this.#settings.key,\r\n );\r\n }\r\n\r\n #setStatus(message: string, tone: 'info' | 'error' = 'info') {\r\n if (this.#refs) {\r\n this.#refs.status.textContent = message;\r\n this.#refs.status.dataset.tone = tone;\r\n }\r\n }\r\n\r\n #loadChords(signal: AbortSignal) {\r\n this.player.loadChords().then(\r\n () => {\r\n if (signal.aborted || !this.#root || !this.#refs) {\r\n return;\r\n }\r\n\r\n this.#renderVoicing();\r\n this.#chordLibrary = mountChordLibrary({\r\n root: this.#root,\r\n groups: this.#refs.chords,\r\n summary: this.#refs.chordsSummary,\r\n cardId: `${this.#prefix}-chord-card`,\r\n player: this.player,\r\n signal,\r\n getInstrument: () => this.#getInstrument(),\r\n getKey: () => this.#settings.key,\r\n onError: (message) => this.#setStatus(message, 'error'),\r\n });\r\n },\r\n (error) => {\r\n if (signal.aborted || !this.#refs) {\r\n return;\r\n }\r\n\r\n this.#refs.voicing.textContent = '';\r\n this.#setStatus(\r\n `Could not load chord voicings: ${error instanceof Error ? error.message : String(error)}`,\r\n 'error',\r\n );\r\n },\r\n );\r\n }\r\n\r\n // ——— Song controls ———\r\n\r\n #renderSongControls() {\r\n const refs = this.#refs!;\r\n const settings = this.#settings;\r\n\r\n refs.key.replaceChildren(\r\n ...KEYS.map((key) => new Option(key, key, false, key === settings.key)),\r\n );\r\n refs.time.replaceChildren(\r\n ...TIME_SIGNATURES.map(\r\n (time) => new Option(time, time, false, time === settings.time),\r\n ),\r\n );\r\n refs.tempo.value = String(settings.tempo);\r\n refs.tempoRange.value = String(settings.tempo);\r\n }\r\n\r\n #setTempo(value: unknown) {\r\n const refs = this.#refs!;\r\n const tempo = clampTempo(value) ?? this.#settings.tempo;\r\n\r\n this.#settings.tempo = tempo;\r\n refs.tempo.value = String(tempo);\r\n refs.tempoRange.value = String(tempo);\r\n }\r\n\r\n // ——— Instruments ———\r\n\r\n #renderInstruments() {\r\n this.#refs!.instruments.replaceChildren(\r\n ...STRUM_INSTRUMENTS.map((instrument) => {\r\n const label = document.createElement('label');\r\n const input = document.createElement('input');\r\n const text = document.createElement('span');\r\n const name = document.createElement('span');\r\n const sound = document.createElement('span');\r\n const toggle = document.createElement('span');\r\n\r\n label.className = 'instrument';\r\n input.type = 'radio';\r\n input.name = `${this.#prefix}-instrument`;\r\n input.value = instrument.id;\r\n input.checked = instrument.id === this.#settings.instrument;\r\n text.className = 'instrument__text';\r\n name.className = 'instrument__name';\r\n name.textContent = instrument.label;\r\n sound.className = 'instrument__sound';\r\n sound.textContent = instrument.soundfont.title;\r\n toggle.className = 'instrument__switch';\r\n toggle.setAttribute('aria-hidden', 'true');\r\n text.append(name, sound);\r\n label.append(input, text, toggle);\r\n return label;\r\n }),\r\n );\r\n }\r\n\r\n #renderVoicing() {\r\n const refs = this.#refs!;\r\n\r\n if (!this.player.chordsLoaded) {\r\n refs.voicing.textContent = 'Loading chord voicings…';\r\n return;\r\n }\r\n\r\n const voicing = this.#getVoicing();\r\n const instrument = this.#getInstrument();\r\n\r\n if (!voicing) {\r\n refs.voicing.textContent = `No ${instrument.label} voicing for the key of ${this.#settings.key}.`;\r\n return;\r\n }\r\n\r\n const summary = document.createElement('div');\r\n const notes = document.createElement('div');\r\n\r\n summary.append('Strums ');\r\n summary.append(\r\n Object.assign(document.createElement('strong'), {\r\n textContent: voicing.chord,\r\n }),\r\n );\r\n\r\n if (voicing.card.kind === 'diagram') {\r\n const { frets, baseFret } = voicing.card.voicing;\r\n\r\n summary.append(' — frets ');\r\n summary.append(\r\n Object.assign(document.createElement('code'), {\r\n textContent: frets\r\n .map((fret) =>\r\n fret < 0 ? 'x' : String(fret === 0 ? 0 : fret + baseFret - 1),\r\n )\r\n .join(' '),\r\n }),\r\n );\r\n }\r\n\r\n notes.className = 'voicing__notes';\r\n notes.append(\r\n ...voicing.midi.map((midi) =>\r\n Object.assign(document.createElement('span'), {\r\n className: 'voicing__note',\r\n textContent: getMidiNoteName(midi, this.#settings.key),\r\n }),\r\n ),\r\n );\r\n refs.voicing.replaceChildren(summary, notes);\r\n }\r\n\r\n // ——— Patterns ———\r\n\r\n #findCard(patternId: string): HTMLElement | null {\r\n return (\r\n this.#refs?.patterns.querySelector<HTMLElement>(\r\n `.pattern-card[data-pattern-id=\"${CSS.escape(patternId)}\"]`,\r\n ) ?? null\r\n );\r\n }\r\n\r\n #renderPatterns() {\r\n const refs = this.#refs!;\r\n\r\n this.#patterns = new Map();\r\n\r\n for (const definition of this.#settings.patterns) {\r\n try {\r\n this.#patterns.set(\r\n definition.id,\r\n parseOpenLyricPattern(definition, this.#settings.time),\r\n );\r\n } catch (error) {\r\n console.warn(`Skipping pattern ${definition.label}:`, error);\r\n }\r\n }\r\n\r\n this.#currentStep = null;\r\n refs.patterns.replaceChildren(\r\n ...Array.from(this.#patterns.values(), createPatternCard),\r\n );\r\n refs.patternsEmpty.hidden = this.#patterns.size > 0;\r\n this.#syncPlaybackState();\r\n }\r\n\r\n #syncPlaybackState() {\r\n const refs = this.#refs;\r\n\r\n if (!refs) {\r\n return;\r\n }\r\n\r\n for (const card of refs.patterns.querySelectorAll<HTMLElement>(\r\n '.pattern-card',\r\n )) {\r\n const isActive = card.dataset.patternId === this.player.patternId;\r\n const state = isActive ? this.player.state : 'idle';\r\n const play = card.querySelector<HTMLButtonElement>(\r\n '.pattern-card__play',\r\n )!;\r\n const label =\r\n this.#patterns.get(card.dataset.patternId!)?.label ?? 'pattern';\r\n\r\n card.dataset.state = state;\r\n play.setAttribute('aria-pressed', String(state !== 'idle'));\r\n play.setAttribute(\r\n 'aria-label',\r\n state === 'idle' ? `Play ${label}` : `Stop ${label}`,\r\n );\r\n play.title = state === 'idle' ? `Play ${label}` : `Stop ${label}`;\r\n }\r\n }\r\n\r\n #showCurrentStep(patternId: string, stepNumber: number | null) {\r\n this.#currentStep?.classList.remove('is-current');\r\n this.#currentStep =\r\n stepNumber == null\r\n ? null\r\n : (this.#findCard(patternId)?.querySelector<HTMLElement>(\r\n `.step[data-step=\"${stepNumber}\"]`,\r\n ) ?? null);\r\n this.#currentStep?.classList.add('is-current');\r\n }\r\n\r\n #startPattern(patternId: string) {\r\n const pattern = this.#patterns.get(patternId);\r\n\r\n if (!pattern) {\r\n return;\r\n }\r\n\r\n if (!this.#getVoicing()) {\r\n this.#setStatus(\r\n this.player.chordsLoaded\r\n ? `No ${this.#getInstrument().label} voicing for the key of ${this.#settings.key}.`\r\n : 'Chord voicings are still loading.',\r\n 'error',\r\n );\r\n return;\r\n }\r\n\r\n this.player\r\n .playPattern({\r\n pattern,\r\n key: this.#settings.key,\r\n tempo: this.#settings.tempo,\r\n time: this.#settings.time,\r\n instrument: this.#settings.instrument,\r\n })\r\n .catch((error) =>\r\n this.#setStatus(\r\n `Could not play: ${error instanceof Error ? error.message : String(error)}`,\r\n 'error',\r\n ),\r\n );\r\n }\r\n\r\n /** Picks up a changed key, time, tempo or instrument without a new press. */\r\n #restartIfPlaying() {\r\n const { patternId } = this.player;\r\n\r\n if (\r\n this.player.state !== 'idle' &&\r\n patternId &&\r\n this.#patterns.has(patternId)\r\n ) {\r\n this.#startPattern(patternId);\r\n }\r\n }\r\n\r\n #cycleStep(patternId: string, index: number) {\r\n const settings = this.#settings;\r\n const position = settings.patterns.findIndex(\r\n (definition) => definition.id === patternId,\r\n );\r\n\r\n if (position === -1) {\r\n return;\r\n }\r\n\r\n const definition = cycleStrumPatternStep(\r\n settings.patterns[position],\r\n index,\r\n settings.time,\r\n );\r\n const pattern = parseOpenLyricPattern(definition, settings.time);\r\n const card = this.#findCard(patternId);\r\n\r\n settings.patterns[position] = definition;\r\n this.#patterns.set(patternId, pattern);\r\n this.#saveSettings();\r\n\r\n // Refill in place, so the pressed step keeps focus.\r\n card\r\n ?.querySelectorAll<HTMLElement>('.step')\r\n .forEach((element, stepIndex) => fillStep(element, pattern, stepIndex));\r\n\r\n const source = card?.querySelector('.pattern-card__source code');\r\n\r\n if (source) {\r\n source.textContent = pattern.source;\r\n }\r\n\r\n this.player.updatePattern(pattern);\r\n }\r\n\r\n #removePattern(patternId: string) {\r\n const pattern = this.#patterns.get(patternId);\r\n\r\n if (this.player.patternId === patternId) {\r\n this.player.stop();\r\n }\r\n\r\n this.#settings.patterns = this.#settings.patterns.filter(\r\n (definition) => definition.id !== patternId,\r\n );\r\n this.#saveSettings();\r\n this.#renderPatterns();\r\n this.#setStatus(pattern ? `Removed ${pattern.label}.` : '');\r\n }\r\n\r\n #createPatternId() {\r\n this.#idCounter += 1;\r\n return `custom-${Date.now().toString(36)}-${this.#idCounter}`;\r\n }\r\n\r\n #createLabeler() {\r\n let next =\r\n this.#settings.patterns.reduce((highest, definition) => {\r\n const match = /^Pattern (\\d+)$/.exec(definition.label);\r\n\r\n return match ? Math.max(highest, Number(match[1])) : highest;\r\n }, 0) + 1;\r\n\r\n return () => `Pattern ${next++}`;\r\n }\r\n\r\n #addPatterns() {\r\n const refs = this.#refs!;\r\n\r\n refs.addError.textContent = '';\r\n\r\n try {\r\n const added = readStrumPatternInput(\r\n refs.input.value,\r\n () => this.#createPatternId(),\r\n this.#createLabeler(),\r\n );\r\n\r\n this.#settings.patterns.push(...added);\r\n this.#saveSettings();\r\n this.#renderPatterns();\r\n refs.input.value = '';\r\n this.#setStatus(\r\n `Added ${added.map((definition) => definition.label).join(', ')}.`,\r\n );\r\n } catch (error) {\r\n refs.addError.textContent =\r\n error instanceof Error ? error.message : String(error);\r\n }\r\n }\r\n\r\n #restoreExamplePatterns() {\r\n const others = this.#settings.patterns.filter(\r\n (definition) =>\r\n !EXAMPLE_PATTERNS.some((example) => example.id === definition.id),\r\n );\r\n\r\n if (\r\n EXAMPLE_PATTERNS.some((example) => example.id === this.player.patternId)\r\n ) {\r\n this.player.stop();\r\n }\r\n\r\n this.#settings.patterns = [...copyExamplePatterns(), ...others];\r\n this.#saveSettings();\r\n this.#renderPatterns();\r\n this.#setStatus('Restored the example patterns.');\r\n }\r\n\r\n // ——— Wiring ———\r\n\r\n #onPlayerState({ state, patternId, error }: OpenLyricPlayerStateEvent) {\r\n this.#syncPlaybackState();\r\n\r\n if (state !== 'playing') {\r\n this.#showCurrentStep('', null);\r\n }\r\n\r\n const pattern = patternId ? this.#patterns.get(patternId) : null;\r\n\r\n // A pattern the host started on a shared player is not ours to report.\r\n if (patternId && !pattern) {\r\n return;\r\n }\r\n\r\n if (state === 'loading') {\r\n this.#setStatus(`Loading ${this.#getInstrument().soundfont.title}…`);\r\n } else if (state === 'playing' && pattern) {\r\n this.#setStatus(\r\n `Playing ${pattern.label} — ${this.#getVoicing()?.chord ?? this.#settings.key} on ${this.#getInstrument().label}, ${this.#settings.tempo} bpm, ${this.#settings.time}.`,\r\n );\r\n } else if (error) {\r\n this.#setStatus(\r\n `Could not play: ${error instanceof Error ? error.message : String(error)}`,\r\n 'error',\r\n );\r\n } else {\r\n this.#setStatus('');\r\n }\r\n }\r\n\r\n #wire(signal: AbortSignal) {\r\n const refs = this.#refs!;\r\n const settings = this.#settings;\r\n const listen = { signal };\r\n\r\n this.#unsubscribe.push(\r\n this.player.on('state', (event) => this.#onPlayerState(event)),\r\n this.player.on('step', ({ patternId, stepNumber }) =>\r\n this.#showCurrentStep(patternId, stepNumber),\r\n ),\r\n );\r\n\r\n refs.patterns.addEventListener(\r\n 'click',\r\n (event) => {\r\n const target = (event.target as Element).closest<HTMLElement>(\r\n '[data-action]',\r\n );\r\n const patternId =\r\n target?.closest<HTMLElement>('.pattern-card')?.dataset.patternId;\r\n\r\n if (!target || !patternId) {\r\n return;\r\n }\r\n\r\n switch (target.dataset.action) {\r\n case 'toggle':\r\n if (\r\n this.player.patternId === patternId &&\r\n this.player.state !== 'idle'\r\n ) {\r\n this.player.stop();\r\n } else {\r\n this.#startPattern(patternId);\r\n }\r\n break;\r\n case 'remove':\r\n this.#removePattern(patternId);\r\n break;\r\n case 'cycle':\r\n this.#cycleStep(patternId, Number(target.dataset.step) - 1);\r\n break;\r\n }\r\n },\r\n listen,\r\n );\r\n\r\n refs.key.addEventListener(\r\n 'change',\r\n () => {\r\n settings.key = refs.key.value;\r\n this.#saveSettings();\r\n this.#renderVoicing();\r\n // Note names are spelled the way the song's key spells them.\r\n this.#chordLibrary?.refresh();\r\n this.#restartIfPlaying();\r\n },\r\n listen,\r\n );\r\n\r\n refs.time.addEventListener(\r\n 'change',\r\n () => {\r\n settings.time = refs.time.value;\r\n this.#saveSettings();\r\n this.#renderPatterns();\r\n this.#restartIfPlaying();\r\n },\r\n listen,\r\n );\r\n\r\n refs.tempoRange.addEventListener(\r\n 'input',\r\n () => this.#setTempo(refs.tempoRange.value),\r\n listen,\r\n );\r\n\r\n for (const input of [refs.tempo, refs.tempoRange]) {\r\n input.addEventListener(\r\n 'change',\r\n () => {\r\n this.#setTempo(input.value);\r\n this.#saveSettings();\r\n this.#restartIfPlaying();\r\n },\r\n listen,\r\n );\r\n }\r\n\r\n refs.instruments.addEventListener(\r\n 'change',\r\n (event) => {\r\n const instrument = getStrumInstrument(\r\n (event.target as HTMLInputElement).value,\r\n );\r\n\r\n if (!instrument) {\r\n return;\r\n }\r\n\r\n settings.instrument = instrument.id;\r\n this.#saveSettings();\r\n this.#renderVoicing();\r\n this.#chordLibrary?.refresh();\r\n\r\n if (this.player.state !== 'idle') {\r\n this.#restartIfPlaying();\r\n } else {\r\n // Warm the soundfont up while the choice is fresh (this is a gesture).\r\n this.player.preload(instrument.id).catch(() => {});\r\n }\r\n },\r\n listen,\r\n );\r\n\r\n refs.add.addEventListener('click', () => this.#addPatterns(), listen);\r\n refs.input.addEventListener(\r\n 'keydown',\r\n (event) => {\r\n if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {\r\n event.preventDefault();\r\n this.#addPatterns();\r\n }\r\n },\r\n listen,\r\n );\r\n refs.restore.addEventListener(\r\n 'click',\r\n () => this.#restoreExamplePatterns(),\r\n listen,\r\n );\r\n }\r\n}\r\n","/**\r\n * `open-lyric-plugin-player`'s own version, read from this package's\r\n * `package.json` — the plugin's version, not the core's (`version` from\r\n * `open-lyric` is that one, and the two bump independently).\r\n *\r\n * See `packages/open-lyric/src/version.ts` for why the manifest is imported\r\n * rather than the number restated, and why the import is a named one.\r\n */\r\n\r\nimport { version as manifestVersion } from '../package.json';\r\n\r\nexport const version: string = manifestVersion;\r\n"],"file":"index.js"}