├── README.md ├── c ├── example.c └── pl_synth.h ├── example.html ├── js └── pl_synth.js ├── makefile ├── release ├── pl_synth.min.js └── pl_synth_wasm.min.js ├── tracker.html └── wasm ├── pl_synth_wasm_module.c └── pl_synth_wasm_template.js /README.md: -------------------------------------------------------------------------------- 1 | # PL_SYNTH 2 | 3 | Create raw samples for sound effects and music in JS and C/C++. The synthesizer is a port of [Sonant](https://www.pouet.net/prod.php?which=53615), the tracker is written from scratch. 4 | 5 | 6 | ## Release Version 7 | 8 | pl_synth is implemented in multiple different ways. The release files are self-contained, i.e. you only need to use one of these files in your project. 9 | 10 | - `release/pl_synth.min.js` - the minified version of the vanilla JS implementation. 1.2kb gzipped. 11 | - `release/pl_synth_wasm.min.js` - the minified version of the JS+WASM implementation. The compiled WASM is embedded. 2.5kb gzipped, but about twice as fast as vanilla JS. Recommended if you're not size-restricted. 12 | - `c/pl_synth.h` - a single header library for C/C++ 13 | 14 | 15 | 16 | ## Tracker software 17 | 18 | Songs and sound effects can be created with the tracker software contained in `tracker.html`. After cloning this repo, the tracker can be launched with a simple doubleclick (i.e. it loads fine from a `file://` url). The only requirement for the tracker is `release/pl_synth_wasm.min.js`. 19 | 20 | An online version can be found at 21 | https://phoboslab.org/synth/ 22 | 23 | 24 | ## Demo Songs 25 | 26 | * [Drop](https://phoboslab.org/synth/#eJytk9mNwCAMRBuaD8xhoBaU/tvYGSCH9pKyWpDBNiaYZzJaboYxRgWbRYNhqt2Q6AhBzohSLMEocFjnkkIdHKAtsR8YnHAKTo3+YanBcsGcS8B72287t7e2Ukvl4an4m5227W/t4xCGhbgLixPwqUcdwyPYYG6FahbT3iJaF+TCoVYhTXCRvujScfWLNGUfHDC/dbUJhrIJreUTD2Xn/XXbt8vSKNpPVIrL+9aaywxTKlkH0r0jPgcmkdBAM0puWoLDDQ91vkgkT8KSCaDQloZY5xNcHbC7LzDPK6wjC/7Xu/JuK1mlOQseTWWLFpjqdHvTT+UBmTUPs/p0IvEu8Pk/ZXj47TILYbjK85N6qH0A4LHEdA==) by [no-fate.net](http://no-fate.net) 27 | * [Q1k3](https://phoboslab.org/synth/#eJxtkVuWxCAIRDdUHzwEdS2e7H8bA5g40+nBIwI+uKksJ25Ya3UADDED/oRgIkBdPfIGAfOcFTZ0vrAY3wN1/4w4tVgcOXH7Yx8b15Vnaz+7CyLk8STJEgYLGn2SwoFElFAD0uIJZAmKdq/2zrNNwquUV7yMZeSGWHnaXCkLRo8XRp3xeCnh4k2eWpCBxS6w+OpAG9mNJ5UahOmF9lLnKNQmcmL74z5rm6T6+y1QEDSNziVCN7IMUg/Kn+Q8CIMj4lnX4gorH5KHJmW516JRObtbHta+Ffuq0/91sV817Ur7AYzBfQA=) by [no-fate.net](http://no-fate.net) 28 | * [Underrun](https://phoboslab.org/synth/#eJyNU1mWwyAMu5A+4o3lLLzc/xojmzbtdNo3JcEEb8jCWRFiWGt1cGgEFAOQsTfGRY7uiGbNIEcOyJx0mz4wFR3tQJkYxp3YgJ1YSiXj8OXKiMUpqngZopIG8ZLzo/nVcDlIP89MXyXK3AdyRCoStwQn62u0CgHFMBYYTjdmB9WermmOqgwf33sZJkx9iVwshZe4w9FkTGYDCkkxvhGh0CQERROtL+57psbbJzOKH1fRnz6fqOg9z8zLHoWApYZ1S42HB5FmSzCa52uxludXn8Dr3v5ZvyPjrUO7HGw7bNyxeTrujFFKJ/pm7DohaC/6CNVCC7uDzVoR6d38VsEb9i6SrP9pIbu10KjQofsv2e0X+8bmMcmUZfmhVLTssgSO9SvVQ+wr4+mPeeb4AQ37t8M=) by [no-fate.net](http://no-fate.net) 29 | * [Voidcall](https://phoboslab.org/synth/#eJytVFmSxCAIvRAfsoh6llTuf43hoVl60l0zH52UBAEJ8MDNnStt29YoHu6FKLgRLDsEo1HvxMXZyCrERSpJjUVsTkrWdtqYXt6QbBzmnEdqyRVEc12S/Odh99ZQsPYdDg3WIop/wprE4IJLnCqjDGJRI3ZBaBGmpYFTnalFXr2En7mR9DK/QpdMMnQc+RpBeSIo+hpJhyjst0g6BOb/Ie0NeThMwFZLocAdDLAuqHZAFg9VV9djw2OESoKTamEvFi4mSrrQqvTXXhM9Yfr1hAg5flb4U+GpGE/FgEL1oVC+25wViL5C6osVfM0iWOvUWxMaCJ9LdLqYhpnKsiVvZ7u+vjdZJqw2A7BF5q6ewje6G0IYZgVCk2XKgSxqLQxbcRpWelQYVsCH40IAPgt9ws2gK9KzAseAzfg0S9ZBjT+Uz/Ri+R5etAglPjzgdMagEVxrnjLDrRDNIGBZ6vLC/imoNfVrf0582uTQejmGNmVz7uyYuylrp+w4e/uTt4udOIRp5uQTO4+rq18sR1/EzYfUvGM0KuJcd61F+tEVlZxxILblV2NcGZ5NAZTP5n3HsuFe3fcfSjkwMg==) by [no-fate.net](http://no-fate.net) 30 | * [Liver](https://phoboslab.org/synth/#eJy9VFuS4zAIvFB/CNDzLK7c/xrbgOLYmUpVdj7GiWQESKbbjY/WxHAcxwAvWQoDTf61CG2MuSBqFWLdOjMEijZtQVsFHSo+V+4IY7UHDs/xtfrg+hDmYk8MRfrzeo+1LKV1P+gX+9bj4Y+MlbbGIibdk6Vk8bC1BhrRGKT4tdOM6N2a6AsNPSBV8FzbmC4jUFnxgFo+cWZd+9YLWkGVhdrLIH3FKyhm3K4MQDofIsXTeZ+DRwQAiB92gvVbmpf56viY8eJBAhcuptcmXt90+D5YsBNRA+r5C5gv1j+aPzinufL1kHGtzn8ZjrcLXbVQW7p6Ek7ZyHDhmdfHJK0nGX8goaDro4S8J1zZASdsvsvhnJG9LrKda1Jni3nqeEZoxyikUznmY6sGyKlqWufq7sbXqSlDICdraZ2ruxtfp/qxYcYUPHnKc3V34+vUN7FYtkzoUpTUksmZXZrhWtiL/g2qMPN+rsmuzpDJqyvt2Tm63+22z9WnWJ6TQqgXdV+2PWP3yP/tvbTK/WPhOKUWY+ZSfhjErIqLbMJbhFKjU0Og9JSRoJts/O/t+vDrH/UpKmA=) by m / Bits'n'Bites 31 | * [Regressions](https://phoboslab.org/synth/#eJyFVdexxDAIbIgPEZRq8Vz/bbwFLKcLz55DSAJZu4TbamGmbds64RFuxAQVayRaMaE+JrGoEZtUqGYkNIpAhbVC4kdcKrx8Wgfxi7Y4Dq+vrfG6ZngrNfLTqs/hs+ETbpNDSBesZc0O7bcRzmLc3vdjCOkCQNbs0N6N4ijV86iRVmNJFyxtzQ7tl5GqHyUheYTeXi9H7dQzScXnLyrVAjIL2ShQxHlq0KTazm4Qeb56mx960hrRdX76c/i+tz7z5VnejsQskYzc2odWHIPxJGulB62eKKq4nGCDkD9gu7g5xpFf/IDhgY3fsbk84JxL97Wfpk/LD+gPqr4dvtjoZ1wjlpqMRFgZTIiQoYyaIjFy30qDe/dysOr1ZVlNs13usXDTNy5ul+TqZ6Bin7ePtfdLil+Sx5owd9JSBO2gFrgOuKHkp9evZB7CoUc5mzjsiJjS/2MMq/4ziMxnWbDt5Rtl7bS2OD9mM3Bx7XtdMujiqBS74MlrJrQSnYdLVhNr0xZNCm1n6AwcWADOtgfUlVkfvH9CQtfZkYspajmEHOIakNV60kUOv+nA0tKWY+6ca/0WWExaW2ENrQebLdvkZeDGy86vdq6na1isTWhJaLRG/DOYF2moBaqg/8OsSkMMBtLGq1upT29UVsBh9wiLSjIqym8VlczJ/pcQv7cs/qFmt7j1sGc7+1CxCcpiaahfmVbLRUagMKf1QTobA4R0lGVWqBZYM5IJ+dYLUp+5BoKOIo3w36F5clD26Iu84Ns9QPbLnz/gnn0d) by m / Bits'n'Bites 32 | * [Beatnic](https://phoboslab.org/synth/#eJydVmua5CAIvBA/xLdnydf3v8ZSgMak0z0zu9lWBB9YFDhHCb3TcRyNiJhiKUSbSBwCURktEDfRUZFxLFnsiSlT5BcdjNnEdPbJ+7gk6DCGJut3zpj2InsdnHE8/amDDzZ4+qemS3PO/213No/rt6PM9nrhLlV1IwqCRN1FioAwoOk5BlMyh1gAucP5+ftqvHyKZirLtxROcdear93G3rEEv8C3JnFvVZqKLUMbhUYiXIhDt763rxwwzYz5bNnZMGdk/5QDtxBcwX+L0FtsPhGBOKb12we65r+O3IXJiofmXfjYfNy6DMIPsRkWsjbpFZGyDTjqQHiUinMspyzEgl7w5hQ1rWWn2BCENsAowSBlYMCGEppognfoedm8SRaqC7xwNO4x4Bw2Z3G4OpunTFJ91NFWQnFd4ixEYKpU5QJUM04DEYdt/DU1lFUmJfU0byuswszob2kbPSkebNz1UmkRSOdJ86MuuM4Sz7ZO5dxtP4Qvx2rYdWm5Dq4En5wENRrv+7lBlGDLpxVPNBpTpjQqSwnoWdDpIdbqhjqqBkq9AcixeICKVR1nkZX6ImGcPTTNInM1NBSesYYcNE7MhpUj5nmgUpxS6ku6YWpeGN7AwX9Xc9MjWifG9Vq/mWtVs+xesfoeNSjzWA6UG8QbvOtRTbg0Z5A+Iz2F6ZIBAFbUYQDwhKwwGpEhHVcEef6f3aa5jO+5+QTMQ/TxOqF067vPxdOShz79PQ19+wWpCNesQkEY5eQYX0LfJLCajH0pkPv+6p/zxgz69jSh2nFpLm2+IyFKJue2cOBcEua6GrcFqhbNtGupHud+Yy3N7we18yD1beb43b2V+/XRyR/W2iIj8fMF55z7HV3/9Zqaow839aKUHu/7kTvOdi7RKgmO1PyRmPb051N/sR3+oHq9/gHG1hXB) by m / Bits'n'Bites 33 | * [Frank 4k](https://phoboslab.org/synth/#eJzFV2ly6yAMvpB+ILHYnCXT+1/j6ZNEjImTJm86E1JsWaAFrfRWue10u9020sFdSEjBPuDcad+J616I01ZJcqVM3DgpshlIupRZCQAwE//QDV/yMIkqgaacsPi+mhib8rqxyvziVBU4qw2+N92godGfQMHRJfwJ9FTHMd7X4aB4lMEb//wgKBCvGj1VGU4gla5xqbGZGheSlDS+Gp5Vv0gKZIpHqsYiazSy/TaNOBMUWB0ybSmO1AgWC8ji2fLJy057Paa9vvH+/EhK0BP9F/np8RH5SfByjF9pZyO8MNHYsbiew2mS9A0EXI8pTSuSSIdru4JcUFFEi4ok93IrLm7UouxCKtGMmFYBZohf1H8KxnF+2bra/XcSS58Lc7+wGz1Ex9uGPnoCbGeW4BRWzi1rdnSYqO7aKQAqQuxpsmA1ydYSrhrCdZOgpy0hR+I2y0LJd42PSnPGQq60A7Ef22asbcszJ9MaqEC8XFboEDSE4NNQgXi5rFDoMOm/ugWrOYX/xT2qdY2rWr61tThaPggShCM5lLFoEy9U1W8ZbkzwZO+6rZedOtzcEtkSWVWEcvkD57mTwkaBa+BKfuoejgNLLuPIrjwM0vodknFsX8XFwgxQndYNmIfhvUNMpuOCjXo2pXSjhO/jpefUEC7cqbSkB92hLKec9a01ouBio3ZGYcF7j6SRaBgMHn7WPP1a/GZ4+tnp4a7vzVeFYtp5QWLoY+cKBvBahHtme4fjqvuq0Qn/uGt6vLfV+F5kTx2wZkjtW7L78F40AR2dW92o1yh52pHsKqyR2utkiBE1SJF5jhHf99sFEjusgVSso1tUOdlqrJc++q0RIltOtHfEIA8EgP6USVmZlJXJUFJ4YnI2ol+s/HrGsmlXtj/uOZZLamq3zf6PKLim7cUt2NsSSaMXr2+6ws4XNVO2odg2sTPWA71Bda87M7oNkiV4UZe4QcPGzg/jHzUrmFY=) by m / Bits'n'Bites 34 | * [Synth 4k](https://phoboslab.org/synth/#eJy1VVu26yAInRAfguJjLFmd/zQubDQxuT09H2c1reWlhm5we2hNlY7jGGQPVyYyrZjaxR0pmcYdP0w83ClaqFqoUyZfYGIUkhcdTEKfhn2zbc5Tqm3TqJPa0oOL7317uKRTzd33z/qcc03QbZ4vNPV0rznu9vfMCXpF3m5/LXj3EslYk6DDw8Ni/MxMDCwur5f/yx6+KXy+IUy55gpwmTog12oQCVDyl9BHVH8YANUTjqxP9Q++hdGfd9xdAUwNPAAB8LH+k2jAokVJmjTEs6nm7N48NdlPrw6PBVMFVG5Fh/0iF0AYpW1JTeM7wdmfGCqzFTfjK0GcTmtNjJKjTXfjl2CPoWkdq8v4EGxR24ai9GagK6E5nWhko5eorqA0w53Vq8+avcg8239UyrPCq8rR6p/lvc5IVnGoQ6bL9704ap4DT1XIm3EF09TblNNeGC99xaCnN3HZ43FuLwaaTN6gtmQg9SCikYaDX7xSag4pfsKUnenrhv2F/3+fk3ZuhHm398DeIkOQCvmJ7p6EMJWUPBHjgIL0BjVvpBa90z1zA8tupDFpoFUkcU/1fvE8pc6kUSa5+NzL9LwR+hPMjc5LDyoHY+UAMzuVZ2Qr3v+cJVICITwSXckyWCpk8Rc94DvNc/1+mVX5MQZTefOftP7y5x/Ke5Wt) by m / Bits'n'Bites 35 | * [Chippy](https://phoboslab.org/synth/#eJyVVEmSwzAI/BAHsWh7iyv//8Y0ix07s1SNEiQiI6u7gRxm3Og4jkkYokZMg2jD5w2fWCdJa41sMJP0TkZ7Ca2NOOmYJo4ydrnLiw6m/Mh7xu7BoojKaVzTqsnHXxH+XtxFNZ3HWFtN+YLfI14vB5EcQQHwgyN3C457Ol0l7YC7DDvc8KJky22RUh/fyQUx7XG5Bg22AuNuXMe95RLi6LxFsck7eFdULnYDzNuvczKcP5Rk01rUbSEUcCWo3iEjjxPPRoifeOMcVr0l5IbUpcDkogxXfnrUFTH53O/7jLV5puXGwe9EZIQ7qKl3JgyJaWa44FbDGVTRWENpAxGHjggZUIhBEQTEHDb3kBwEVUL4yFt9kxDw/CDaPn0CPDVUFdbc8TL2VDzqWGnsYP6ZZ+gVhnyHyUgD4DApc+RuqmnW0/59vswcpJTUYSiisL9PvG+C0GH/Pl9m5pbierahCKMAym1wBQWJOh+KPPLiKEtP4/bkhcLTUJaeu/h/QaaruqqjKoGPzrmAPxuq3I9cR1nZ6ZNGSWnfA50yp1TGtYmcLQJYADsC0fLNq96jVx6I3sNbE6nIdn0+GJcblR89kF2PXvLxBa4EFJg=) by m / Bits'n'Bites 36 | * [Fabrik](https://phoboslab.org/synth/#eJytVG2SwyAIvRA/REHxLE7uf43lEZttTNPuztQmo8gDHh/paNaUxhicyFdlIs7UCJvihqTllskq+bWlRFkSGRWhstFgwi/HWxxLU2qkrh1cMlxw6fvWYpPTpcgJAru3wHkp9QSBGfYcwvl0Cv4SoV90kr/gRMSd/LMQE9m3DZUPKat6V4zwNIkGRTMl9dRJaqkFFt6xnIVqw2HW4Ogpx/skR19zTMvz8isQZpsiW4j1gnNFMAwgdw/p9GLgGs4kxsZkQVH9aK4QhC8+fFKoBpD3+Qx2lhbOeNSBeNvku5N7xengvCYDsCfK6sG0zuxQL1b0h9XjLc7C1+zMyV30NPKGgTvswRF5G2qpKQqAEqi4PrbHKpFfeEKLCF0VtzUCq7GyX4ILSjDnYw7KX6WL4a3V7/He7IPvD9I7fD2+g/l5zP+RhcRupn11cVHf5fBQvq0Z1g+IaAxv) by wullon / adinpsz 37 | * [Microscope](https://phoboslab.org/synth/#eJzFV1mS6zAIvJA+rN0+iyv3v8bQIGRttmfevKqxS4Eg0NI0UnJGm3Zznudu6LGHM87sJpPuNms8mbbN2EjNJ5/oM5ODtZsLxsVgyOIsPgOFsHLEjzktnKh5E0wkaysxtjy7Ochit0tRn8mQadDT+kiThM3IdPrYwFNKZ2Nn36tnEdb5xiziQEjwnRN/BwrqJq6BXbHzOjnFc58YYhcgQbGJjU5XBk0CJRGYq4mCVeZPJfBpY7JrP+8xuHZzxVm3R50fwMwhDvaowDjhgdvoMenYDtLtjgQfMJNvMplXbCm/MktDAZGxSnKmFENby1Q91V4jmQd9lrF0TlqPWQWi3W7Jqeatz8y2NKv3ulNi4mguUNai8qWoqIoAJmrJAzwC0u92t4QZcGrragPzoXNdOSZMGb6DNUiaGFR86+VevdReoxhKoWuLAm80TjvKS4SFh5N3YbdkZZrhMSYKcgwWQwdGZflimYiElwmRMGcNvPTm8OChAGctIVKAo+2GvQJ3+4xk1Rb12OFG2Z/0la3qGczk4uXGp+igr2x9fwGLW8pm0le2qmeAxPXNDfZRZ1ka/EddCPHPGOheio3OuUlf2fr+/4KBkxbDrK9sff+vMIh8LZBsue1QAYXmIZFEcbnsMnPX4bAtjHaWGWz5BUn7FwdA5FNVZd8/R+ib62k6XZy5k6rdHIOLk0Ldr0F6sep7CV0s9OoobR0/TV+HatvDAr43wE34wm0Ku24L/CwSEcEIviggM7K/2euYc7gWfWqoAdJ0L7jRv33/FBCVQljOfZbGL5dJVqOgjRnrOprO18CeXKPjStERWvdr1AUl+4+7xd14jXle5dMH5JELnOSOC8a7et17XM/l0sLNxkwy1ztmai5qtUui0+SvFIha9OsiWH55sd6OUMrhtgzeC2RtnKf/1kw/N966NfP9UfsVtk/BrxBd7JZfaMpwXLex/Fvkfwp2x89Pex1aAbQ93OI6C81lhlJINKjK3mOOUHnoqdUdCPPpcf/MKA1D3ao/cDWvww9rX7h2Hs8reXC9XclTzCs+M1s+ny+2jDSU) by Ferris / Youth Uprising 38 | * [Chill](https://phoboslab.org/synth/#eJytVW1yxCAIvRA/lCC6Z8n0/tco4Ecw0e62U3cwBJDo48mexMhwnmcCGfGFAKJFrPoBgCGo1CnUV4gxYAKkACyxwZaKGWP4glO1lUi2vBQg+aYXsbIXSXrGdIAbMSX91N2IZkx/NZIZw2w8zJjvxi/dFcv2SwVJtgpV86hFtrk8UOOGGjXUOoY3xGgjSXJ44V3sErvxQe9okFKZYokXmFDZGVui+WMGVe7woJY80hYrxDtWL51L9HANiBxsb+HK4/kTWhMwTanYPD121E4mnJZ0nN1r6gqOZLmb8t2T3Cpzd3q++jvjKsTALh1sYedgqOzj4KKKm+X20oy2RWIiBZYdOyeodXTcFNRc6a/fe0kKmZZEnMnlQFo4KD2v6AJWv8JoSfx0LEjsVqwcuxWrxvHmHKXXRGushYhWlX4FECr9WzlkylqBVhOpQ+2xokerSRZjguiq8sO4tZTaDYwuQbd2DB4be8ogU1ho6uUwgsPVHP8loWmMV7eIdlBwKshiTUBl/BWxznYz6nHn3/HoqdR+xyOWdrGVufWCUjapEzhrf3jHZTdq/CrDCNzIlXFZ9k8WX60iVvSdKtdewcbOSzXSxMkD9eK3psxhU4Cn4ZOo0YavE65VGMfwp1euzEzRpP4+sd2i3uJac7/varn//cGWcba1qeZ3gbV7tmzMvZw6vgFeG+hz) by Ferris / Youth Uprising 39 | * [Poseidon demo song](https://phoboslab.org/synth/#eJzFF1uS5CDoQnwoUYlnSfX9r7E8RI2ZTrZnu2o7g6CIIi+dI+Wa4DgOAv7FioCwK5kJNkYhABISoLZxzzwhxoAJMAUoEDfkWRS5AcBALziiTGFhx/PPxxJkhtLxOr70edUjZmxr5ORENhS8T05U0SPtp3ld8p8FYsFGN0FXTZkpN4mYtj7QuTnMO/XddKe2z6RW9n10x5I+kzVIyhOGrMKLMNDrJTYt6pJc2dJGIm7isa2GGmCPe5QICBIEm5yl+ZwCmL+hyPHBvN+a4ep5ZOnJ5vDmF1Mw7dT2ohFCMu2SbszqRNavGrWHJSTN6ECwi6lBo8jiCTmayo/AkxXk52Mu47wGHosKYkzHM/3Ib84pNPBM5walDjxoiYLCRxcgHHimH/kNKA08064R1YEHPQe9HMiRx+nq0b2p7N2O4lac0kBVR5fSe0p3oKt1mjVE++Jbn/Oxncga6br5SEsX6mA455RsY41o4Tt85OGV/2TvW/43PP4Yc5PFZ8sbbSlZNbXsnqhA2iFJFSici3xF8D5cNrh0cNrRPq6JIDmGWqh0Aa0ba+K3hHNId8x2Q4ybISbzVugBoCNWiU8zELzEe+XUsprHFC3c08i8iIVoE6evLTIV+IxmbLJjb7uWMKGlSOtFnLStZnAUS5wuZaY2t3YW4gdr3xt2htTBzL2m95xUKyNnSx5t6SpXT0cVg0ykFnjYCtcIOQyXb7mJ5FhIeiLRT2xjJV7+sn6lj05cXlljb5K5ztJPT5n0LZP8OjXC+4LNqdfmBsaKn6KPhLvIqum8ylD6RJ+OcZl0Xv1ry13N9C5am6RFzW6hguzcRkaOFwkdffAIJIkblAfOnCAoIRTslVA8Oab3aupx9O4xu45P5WiOkzvSt+2Dw7g/Cp6Nc5k4h+bF6xdz/4WJfcaSA2d9fzf7pEl3JUB7gakL7ZlnDVBeSlyV/x20SEiFi7S1evD8YfvMi/dz+Gu14F2i3+T+E+taF/4jvOT3B01yu3A=) by Ferris / Youth Uprising 40 | * [Haumea Drums](https://phoboslab.org/synth/#eJx1UVEWwyAIu1A+BIHWs/h6/2ssYNet3Zt99UURksB0l445pwMQqBhwwkaoOuAN0aNDdtkhvqNjG2pQYwCbgbBrpgSBHJiC+ogmspZt53bH7+MzdhyZGUuFU80XpD7qEf5UQ47gPXy0dsqJuPNfVB/O5/FiXCKGsmIKiawxhHZJyQUryoVFlL5F2tUI0XwamZkdcSshyjsrIZT8WOKab+7CVqCSTX8C5n8ybCwP/vaQU6QF1urpwGp6WoOMNc7Ont0dtCIUulfZfuQv5iWsdj9yvQAzu3WO) by Ferris / Youth Uprising 41 | * [Ambidumbi](https://phoboslab.org/synth/#eJy1VEuShSAMvFAWJhDAs1je/xrTHUDR92Yxi8HSBAKk82kP1c1cjuPYBcPcJQlUzXMiunGINW06J7oVmFKF2po0UYNJzLBbMmQ75aBuj/dagfVQS6IJR5OL+ibrUN2HTWnD9ljJcJxxyu21u/TdtGP3yevrDMcAT7TNyYzASyrpCmffYTJG6Rn7LeOKhEjer0tZ5k5HeIkP0bwGIIdBPw0lIvpi0G7oIbS+OgThX9l/iN0I3qkSnXrErgQY98n9LL5EowrI/htF8tu/ht+JASPSZgktUjM9WolSizzdDG26mG5CLj5/sV3f34Cooo+aRmXZlixbklKAqDZoNXVIKKZ/4HvCu1zfiL6qHcrFkiCJDZ2lIGc8u1S2sqEVjZUYzmXhgFwMYPC5ypRLCTJTcS9Fd4MjlPjgSCZBavRXu4mRb2JoSnEN5WAGAZFr+RUK1oHZHhQRQSw58mpIKRZ2Zli3kVhAGCS/G+pB9khtL28dmDv4DndgpjN8OniXvx4ggP/wEblminOkeu3CZv2PMpMXudq3HfVORsrxL1LSIzUi1p/xv7ha6yEWLnyKlRHn+QMg5yGw) by Gargaj / Ümlaüt Design 42 | 43 | 44 | 45 | ## Usage 46 | 47 | In a browser, load the `pl_synth_wasm.min.js`, instantiate pl_synth and then 48 | use `synth.sound()` and `synth.song()` to create WebAudio [AudioBuffers](https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer). These 49 | AudioBuffers can be re-used for multiple [AudioBufferSourceNodes](https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode), so you only need to generate them once. 50 | 51 | #### `pl_synth_wasm_init(audioContext, callback(synth))` 52 | 53 | When using the JS+WASM version, this asynchronously instantiates pl_synth with the given 54 | [AudioContext](https://developer.mozilla.org/en-US/docs/Web/API/AudioContext). 55 | 56 | #### `pl_synth_init(audioContext)` 57 | 58 | When using the vanilla JS version, this instantiates pl_synth with the given [AudioContext](https://developer.mozilla.org/en-US/docs/Web/API/AudioContext). Returns the synth instance. 59 | 60 | #### `synth.sound(instrument, note = 147, row_len = 5513)` 61 | 62 | Creates a single sound effect with the given instrument data (as created with the tracker), and an optional note and row_len. The default note value 147 correspondes to a C-5 note. The default row_len correspondes to 120 BPM. Returns a WebAudio AudioBuffer. 63 | 64 | #### `synth.song(songData)` 65 | 66 | Creates a whole song with the given songData (as created with the tracker). Returns a WebAudio AudioBuffer. 67 | 68 | ### Synopsis 69 | 70 | ```html 71 | 72 | 90 | ``` 91 | 92 | Also have a look at `example.html`. 93 | 94 | ### Native C/C++ Version 95 | 96 | For usage of the native C/C++ version please refer to the documentation within `c/pl_synth.h` and have a look at 97 | `c/example.c`. This uses `pl_synth.h` to generate some music and saves it as `example.wav` 98 | 99 | -------------------------------------------------------------------------------- /c/example.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define PL_SYNTH_IMPLEMENTATION 8 | #include "pl_synth.h" 9 | 10 | 11 | // WAV writer ------------------------------------------------------------------ 12 | 13 | #define CHUNK_ID(S) \ 14 | (((unsigned int)(S[3])) << 24 | ((unsigned int)(S[2])) << 16 | \ 15 | ((unsigned int)(S[1])) << 8 | ((unsigned int)(S[0]))) 16 | 17 | void fwrite_u32_le(unsigned int v, FILE *fh) { 18 | uint8_t buf[sizeof(unsigned int)]; 19 | buf[0] = 0xff & (v ); 20 | buf[1] = 0xff & (v >> 8); 21 | buf[2] = 0xff & (v >> 16); 22 | buf[3] = 0xff & (v >> 24); 23 | int wrote = fwrite(buf, sizeof(unsigned int), 1, fh); 24 | assert(wrote); 25 | } 26 | 27 | void fwrite_u16_le(unsigned short v, FILE *fh) { 28 | uint8_t buf[sizeof(unsigned short)]; 29 | buf[0] = 0xff & (v ); 30 | buf[1] = 0xff & (v >> 8); 31 | int wrote = fwrite(buf, sizeof(unsigned short), 1, fh); 32 | assert(wrote); 33 | } 34 | 35 | int wav_write(const char *path, short *samples, int samples_len, short channels, int samplerate) { 36 | unsigned int data_size = samples_len * channels * sizeof(short); 37 | short bits_per_sample = 16; 38 | 39 | /* Lifted from https://www.jonolick.com/code.html - public domain 40 | Made endian agnostic using fwrite() */ 41 | FILE *fh = fopen(path, "wb"); 42 | assert(fh); 43 | fwrite("RIFF", 1, 4, fh); 44 | fwrite_u32_le(data_size + 44 - 8, fh); 45 | fwrite("WAVEfmt \x10\x00\x00\x00\x01\x00", 1, 14, fh); 46 | fwrite_u16_le(channels, fh); 47 | fwrite_u32_le(samplerate, fh); 48 | fwrite_u32_le(channels * samplerate * bits_per_sample/8, fh); 49 | fwrite_u16_le(channels * bits_per_sample/8, fh); 50 | fwrite_u16_le(bits_per_sample, fh); 51 | fwrite("data", 1, 4, fh); 52 | fwrite_u32_le(data_size, fh); 53 | fwrite((void*)samples, data_size, 1, fh); 54 | fclose(fh); 55 | return data_size + 44 - 8; 56 | } 57 | 58 | 59 | 60 | // Song data ------------------------------------------------------------------- 61 | 62 | pl_synth_song_t song = { 63 | .row_len = 8481, 64 | .num_tracks = 4, 65 | .tracks = (pl_synth_track_t[]){ 66 | { 67 | .synth = {7,0,0,0,121,1,7,0,0,0,91,3,0,100,1212,5513,100,0,6,19,3,121,6,21,0,1,1,29}, 68 | .sequence_len = 12, 69 | .sequence = (uint8_t[]){1,2,1,2,1,2,0,0,1,2,1,2}, 70 | .patterns = (pl_synth_pattern_t[]){ 71 | {.notes = {138,145,138,150,138,145,138,150,138,145,138,150,138,145,138,150,136,145,138,148,136,145,138,148,136,145,138,148,136,145,138,148}}, 72 | {.notes = {135,145,138,147,135,145,138,147,135,145,138,147,135,145,138,147,135,143,138,146,135,143,138,146,135,143,138,146,135,143,138,146}} 73 | } 74 | }, 75 | { 76 | .synth = {7,0,0,0,192,1,6,0,9,0,192,1,25,137,1111,16157,124,1,982,89,6,25,6,77,0,1,3,69}, 77 | .sequence_len = 12, 78 | .sequence = (uint8_t[]){0,0,1,2,1,2,3,3,3,3,3,3}, 79 | .patterns = (pl_synth_pattern_t[]){ 80 | {.notes = {138,138,0,138,140,0,141,0,0,0,0,0,0,0,0,0,136,136,0,136,140,0,141}}, 81 | {.notes = {135,135,0,135,140,0,141,0,0,0,0,0,0,0,0,0,135,135,0,135,140,0,141,0,140,140}}, 82 | {.notes = {145,0,0,0,145,143,145,150,0,148,0,146,0,143,0,0,0,145,0,0,0,145,143,145,139,0,139,0,0,142,142}} 83 | } 84 | }, 85 | { 86 | .synth = {7,0,0,1,255,0,7,0,0,1,255,0,0,100,0,3636,174,2,500,254,0,27}, 87 | .sequence_len = 12, 88 | .sequence = (uint8_t[]){1,1,1,1,0,0,1,1,1,1,1,1}, 89 | .patterns = (pl_synth_pattern_t[]){ 90 | {.notes = {135,135,0,135,139,0,135,135,135,0,135,139,0,135,135,135,0,135,139,0,135,135,135,0,135,139,0,135,135,135,0,135}} 91 | } 92 | }, 93 | { 94 | .synth = {8,0,0,1,200,0,7,0,0,0,211,3,210,50,200,6800,153,4,11025,254,6,32,5,61,0,1,4,60}, 95 | .sequence_len = 12, 96 | .sequence = (uint8_t[]){1,1,1,1,0,0,1,1,1,1,1,1}, 97 | .patterns = (pl_synth_pattern_t[]){ 98 | {.notes = {0,0,0,0,140,0,0,0,0,0,0,0,140,0,0,0,0,0,0,0,140,0,0,0,0,0,0,0,140}} 99 | } 100 | } 101 | } 102 | }; 103 | 104 | int main(int argc, char **argv) { 105 | // Initialize the instrument lookup table 106 | void *synth_tab = malloc(PL_SYNTH_TAB_SIZE); 107 | pl_synth_init(synth_tab); 108 | 109 | // Determine the number of samples needed for the song 110 | int num_samples = pl_synth_song_len(&song); 111 | printf("generating %d samples\n", num_samples); 112 | 113 | // Allocate buffers 114 | int buffer_size = num_samples * 2 * sizeof(int16_t); 115 | int16_t *output_samples = malloc(buffer_size); 116 | int16_t *temp_samples = malloc(buffer_size); 117 | 118 | // Generate 119 | pl_synth_song(&song, output_samples, temp_samples); 120 | 121 | // Temp buffer not needed anymore 122 | free(temp_samples); 123 | 124 | // Write the generated samples to example.wav 125 | printf("writing example.wav\n"); 126 | wav_write("example.wav", output_samples, num_samples, 2, 44100); 127 | 128 | free(output_samples); 129 | free(synth_tab); 130 | 131 | return 0; 132 | } 133 | -------------------------------------------------------------------------------- /c/pl_synth.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2024, Dominic Szablewski - https://phoboslab.org 4 | SPDX-License-Identifier: MIT 5 | 6 | Based on Sonant, published under the Creative Commons Public License 7 | (c) 2008-2009 Jake Taylor [ Ferris / Youth Uprising ] 8 | 9 | 10 | -- Synopsis 11 | 12 | // Define `PL_SYNTH_IMPLEMENTATION` in *one* C/C++ file before including this 13 | // library to create the implementation. 14 | 15 | #define PL_SYNTH_IMPLEMENTATION 16 | #include "pl_synth.h" 17 | 18 | // Initialize the lookup table for the oscillators 19 | void *synth_tab = malloc(PL_SYNTH_TAB_SIZE); 20 | pl_synth_init(synth_tab); 21 | 22 | // A sound is described by an instrument (synth), the row_len in samples and 23 | // a note. 24 | pl_synth_sound_t sound = { 25 | .synth = {7,0,0,0,192,0,7,0,0,0,192,0,0,200,2000,20000,192}, 26 | .row_len = 5168, 27 | .note = 135 28 | }; 29 | 30 | // Determine the number of the samples for a sound effect and allocate the 31 | // sample buffer for both (stereo) channels 32 | int num_samples = pl_synth_sound_len(&sound); 33 | uint16_t *sample_buffer = malloc(num_samples * 2 * sizeof(uint16_t)); 34 | 35 | // Generate the samples 36 | pl_synth_sound(&sound, sample_buffer); 37 | 38 | See below for a documentation of all functions exposed by this library. 39 | 40 | 41 | */ 42 | 43 | #ifndef PL_SYNTH_H 44 | #define PL_SYNTH_H 45 | 46 | #ifdef __cplusplus 47 | extern "C" { 48 | #endif 49 | 50 | #include 51 | #define PL_SYNTH_SAMPLERATE 44100 52 | #define PL_SYNTH_TAB_LEN 4096 53 | #define PL_SYNTH_TAB_SIZE (sizeof(float) * PL_SYNTH_TAB_LEN * 4) 54 | 55 | typedef struct { 56 | uint8_t osc0_oct; 57 | uint8_t osc0_det; 58 | uint8_t osc0_detune; 59 | uint8_t osc0_xenv; 60 | uint8_t osc0_vol; 61 | uint8_t osc0_waveform; 62 | 63 | uint8_t osc1_oct; 64 | uint8_t osc1_det; 65 | uint8_t osc1_detune; 66 | uint8_t osc1_xenv; 67 | uint8_t osc1_vol; 68 | uint8_t osc1_waveform; 69 | 70 | uint8_t noise_fader; 71 | 72 | uint32_t env_attack; 73 | uint32_t env_sustain; 74 | uint32_t env_release; 75 | uint32_t env_master; 76 | 77 | uint8_t fx_filter; 78 | uint32_t fx_freq; 79 | uint8_t fx_resonance; 80 | uint8_t fx_delay_time; 81 | uint8_t fx_delay_amt; 82 | uint8_t fx_pan_freq; 83 | uint8_t fx_pan_amt; 84 | 85 | uint8_t lfo_osc_freq; 86 | uint8_t lfo_fx_freq; 87 | uint8_t lfo_freq; 88 | uint8_t lfo_amt; 89 | uint8_t lfo_waveform; 90 | } pl_synth_t; 91 | 92 | typedef struct { 93 | pl_synth_t synth; 94 | uint32_t row_len; 95 | uint8_t note; 96 | } pl_synth_sound_t; 97 | 98 | typedef struct { 99 | uint8_t notes[32]; 100 | } pl_synth_pattern_t; 101 | 102 | typedef struct { 103 | pl_synth_t synth; 104 | uint32_t sequence_len; 105 | uint8_t *sequence; 106 | pl_synth_pattern_t *patterns; 107 | } pl_synth_track_t; 108 | 109 | typedef struct { 110 | uint32_t row_len; 111 | uint8_t num_tracks; 112 | pl_synth_track_t *tracks; 113 | } pl_synth_song_t; 114 | 115 | // Initialize the lookup table for all instruments. This needs to be done only 116 | // once. The table will be written to the memory pointed to by tab_buffer, which 117 | // must be PL_SYNTH_TAB_LEN elements long or PL_SYNTH_TAB_SIZE bytes in size. 118 | void pl_synth_init(float *tab_buffer); 119 | 120 | // Determine the number of samples needed for one channel of a particular sound 121 | // effect. 122 | int pl_synth_sound_len(pl_synth_sound_t *sound); 123 | 124 | // Generate a stereo sound into the buffer pointed to by samples. The buffer 125 | // must be at least pl_synth_sound_len() * 2 elements long. 126 | int pl_synth_sound(pl_synth_sound_t *sound, int16_t *samples); 127 | 128 | // Determine the number of samples needed for one channel of a particular song. 129 | int pl_synth_song_len(pl_synth_song_t *song); 130 | 131 | // Generate a stereo song into the buffer pointed to by samples, with temporary 132 | // storage provided to by temp_samples. The buffers samples and temp_samples 133 | // must each be at least pl_synth_song_len() * 2 elements long. 134 | int pl_synth_song(pl_synth_song_t *song, int16_t *samples, int16_t *temp_samples); 135 | 136 | #ifdef __cplusplus 137 | } 138 | #endif 139 | #endif /* PL_SYNTH_H */ 140 | 141 | 142 | 143 | /* ----------------------------------------------------------------------------- 144 | Implementation */ 145 | 146 | #ifdef PL_SYNTH_IMPLEMENTATION 147 | 148 | #include // powf, sinf, logf, ceilf 149 | 150 | #define PL_SYNTH_TAB_MASK (PL_SYNTH_TAB_LEN-1) 151 | #define PL_SYNTH_TAB(WAVEFORM, K) pl_synth_tab[WAVEFORM][((int)((K) * PL_SYNTH_TAB_LEN)) & PL_SYNTH_TAB_MASK] 152 | 153 | static float *pl_synth_tab[4]; 154 | static uint32_t pl_synth_rand = 0xd8f554a5; 155 | 156 | void pl_synth_init(float *tab_buffer) { 157 | for (int i = 0; i < 4; i++) { 158 | pl_synth_tab[i] = tab_buffer + PL_SYNTH_TAB_LEN * i; 159 | } 160 | 161 | // sin 162 | for (int i = 0; i < PL_SYNTH_TAB_LEN; i++) { 163 | pl_synth_tab[0][i] = sinf(i*(6.283184f/(float)PL_SYNTH_TAB_LEN)); 164 | } 165 | // square 166 | for (int i = 0; i < PL_SYNTH_TAB_LEN; i++) { 167 | pl_synth_tab[1][i] = pl_synth_tab[0][i] < 0 ? -1 : 1; 168 | } 169 | // sawtooth 170 | for (int i = 0; i < PL_SYNTH_TAB_LEN; i++) { 171 | pl_synth_tab[2][i] = (float)i / PL_SYNTH_TAB_LEN - 0.5; 172 | } 173 | // triangle 174 | for (int i = 0; i < PL_SYNTH_TAB_LEN; i++) { 175 | pl_synth_tab[3][i] = i < PL_SYNTH_TAB_LEN/2 176 | ? (i/(PL_SYNTH_TAB_LEN/4.0)) - 1.0 177 | : 3.0 - (i/(PL_SYNTH_TAB_LEN/4.0)); 178 | } 179 | } 180 | 181 | static inline float pl_synth_note_freq(int n, int oct, int semi, int detune) { 182 | return (0.00390625 * powf(1.059463094, n - 128 + (oct - 8) * 12 + semi)) * (1.0f + 0.0008f * detune); 183 | } 184 | 185 | static inline int pl_synth_clamp_s16(int v) { 186 | if ((unsigned int)(v + 32768) > 65535) { 187 | if (v < -32768) { return -32768; } 188 | if (v > 32767) { return 32767; } 189 | } 190 | return v; 191 | } 192 | 193 | static void pl_synth_gen(int16_t *samples, int write_pos, int row_len, int note, pl_synth_t *s) { 194 | float fx_pan_freq = powf(2, s->fx_pan_freq - 8) / row_len; 195 | float lfo_freq = powf(2, s->lfo_freq - 8) / row_len; 196 | 197 | // We need higher precision here, because the oscilator positions may be 198 | // advanced by tiny values and error accumulates over time 199 | double osc0_pos = 0; 200 | double osc1_pos = 0; 201 | 202 | float fx_resonance = s->fx_resonance / 255.0f; 203 | float noise_vol = s->noise_fader * 4.6566129e-010f; 204 | float low = 0; 205 | float band = 0; 206 | float high = 0; 207 | 208 | float inv_attack = 1.0f / s->env_attack; 209 | float inv_release = 1.0f / s->env_release; 210 | float lfo_amt = s->lfo_amt / 512.0f; 211 | float pan_amt = s->fx_pan_amt / 512.0f; 212 | 213 | float osc0_freq = pl_synth_note_freq(note, s->osc0_oct, s->osc0_det, s->osc0_detune); 214 | float osc1_freq = pl_synth_note_freq(note, s->osc1_oct, s->osc1_det, s->osc1_detune); 215 | 216 | int num_samples = s->env_attack + s->env_sustain + s->env_release - 1; 217 | 218 | for (int j = num_samples; j >= 0; j--) { 219 | int k = j + write_pos; 220 | 221 | // LFO 222 | float lfor = PL_SYNTH_TAB(s->lfo_waveform, k * lfo_freq) * lfo_amt + 0.5f; 223 | 224 | float sample = 0; 225 | float filter_f = s->fx_freq; 226 | float temp_f; 227 | float envelope = 1; 228 | 229 | // Envelope 230 | if (j < s->env_attack) { 231 | envelope = (float)j * inv_attack; 232 | } 233 | else if (j >= s->env_attack + s->env_sustain) { 234 | envelope -= (float)(j - s->env_attack - s->env_sustain) * inv_release; 235 | } 236 | 237 | // Oscillator 1 238 | temp_f = osc0_freq; 239 | if (s->lfo_osc_freq) { 240 | temp_f *= lfor; 241 | } 242 | if (s->osc0_xenv) { 243 | temp_f *= envelope * envelope; 244 | } 245 | osc0_pos += temp_f; 246 | sample += PL_SYNTH_TAB(s->osc0_waveform, osc0_pos) * s->osc0_vol; 247 | 248 | // Oscillator 2 249 | temp_f = osc1_freq; 250 | if (s->osc1_xenv) { 251 | temp_f *= envelope * envelope; 252 | } 253 | osc1_pos += temp_f; 254 | sample += PL_SYNTH_TAB(s->osc1_waveform, osc1_pos) * s->osc1_vol; 255 | 256 | // Noise oscillator 257 | if (noise_vol) { 258 | int32_t r = (int32_t)pl_synth_rand; 259 | sample += (float)r * noise_vol * envelope; 260 | pl_synth_rand ^= pl_synth_rand << 13; 261 | pl_synth_rand ^= pl_synth_rand >> 17; 262 | pl_synth_rand ^= pl_synth_rand << 5; 263 | } 264 | 265 | sample *= envelope * (1.0f / 255.0f); 266 | 267 | // State variable filter 268 | if (s->fx_filter) { 269 | if (s->lfo_fx_freq) { 270 | filter_f *= lfor; 271 | } 272 | 273 | filter_f = PL_SYNTH_TAB(0, filter_f * (0.5f / PL_SYNTH_SAMPLERATE)) * 1.5f; 274 | low += filter_f * band; 275 | high = fx_resonance * (sample - band) - low; 276 | band += filter_f * high; 277 | sample = (float[5]){sample, high, low, band, low + high}[s->fx_filter]; 278 | } 279 | 280 | // Panning & master volume 281 | temp_f = PL_SYNTH_TAB(0, k * fx_pan_freq) * pan_amt + 0.5f; 282 | sample *= 78 * s->env_master; 283 | 284 | 285 | samples[k * 2 + 0] += sample * (1-temp_f); 286 | samples[k * 2 + 1] += sample * temp_f; 287 | } 288 | } 289 | 290 | static void pl_synth_apply_delay(int16_t *samples, int len, int shift, float amount) { 291 | int len_2 = len * 2; 292 | int shift_2 = shift * 2; 293 | for (int i = 0, j = shift_2; j < len_2; i += 2, j += 2) { 294 | samples[j + 0] += samples[i + 1] * amount; 295 | samples[j + 1] += samples[i + 0] * amount; 296 | } 297 | } 298 | 299 | static int pl_synth_instrument_len(pl_synth_t *synth, int row_len) { 300 | int delay_shift = (synth->fx_delay_time * row_len) / 2; 301 | float delay_amount = synth->fx_delay_amt / 255.0; 302 | float delay_iter = ceilf(logf(0.1) / logf(delay_amount)); 303 | return synth->env_attack + 304 | synth->env_sustain + 305 | synth->env_release + 306 | delay_iter * delay_shift; 307 | } 308 | 309 | int pl_synth_sound_len(pl_synth_sound_t *sound) { 310 | return pl_synth_instrument_len(&sound->synth, sound->row_len); 311 | } 312 | 313 | int pl_synth_sound(pl_synth_sound_t *sound, int16_t *samples) { 314 | int len = pl_synth_sound_len(sound); 315 | pl_synth_gen(samples, 0, sound->row_len, sound->note, &sound->synth); 316 | 317 | if (sound->synth.fx_delay_amt) { 318 | int delay_shift = (sound->synth.fx_delay_time * sound->row_len) / 2; 319 | float delay_amount = sound->synth.fx_delay_amt / 256.0; 320 | pl_synth_apply_delay(samples, len, delay_shift, delay_amount); 321 | } 322 | 323 | return len; 324 | } 325 | 326 | int pl_synth_song_len(pl_synth_song_t *song) { 327 | int num_samples = 0; 328 | for (int t = 0; t < song->num_tracks; t++) { 329 | int track_samples = song->tracks[t].sequence_len * song->row_len * 32 + 330 | pl_synth_instrument_len(&song->tracks[t].synth, song->row_len); 331 | 332 | if (track_samples > num_samples) { 333 | num_samples = track_samples; 334 | } 335 | } 336 | 337 | return num_samples; 338 | } 339 | 340 | int pl_synth_song(pl_synth_song_t *song, int16_t *samples, int16_t *temp_samples) { 341 | int len = pl_synth_song_len(song); 342 | int len_2 = len * 2; 343 | memset(samples, 0, sizeof(int16_t) * len_2); 344 | 345 | for (int t = 0; t < song->num_tracks; t++) { 346 | pl_synth_track_t *track = &song->tracks[t]; 347 | memset(temp_samples, 0, sizeof(int16_t) * len_2); 348 | 349 | for (int si = 0; si < track->sequence_len; si++) { 350 | int write_pos = song->row_len * si * 32; 351 | int pi = track->sequence[si]; 352 | if (pi > 0) { 353 | unsigned char *pattern = track->patterns[pi-1].notes; 354 | for (int row = 0; row < 32; row++) { 355 | int note = pattern[row]; 356 | if (note > 0) { 357 | pl_synth_gen(temp_samples, write_pos, song->row_len, note, &track->synth); 358 | } 359 | write_pos += song->row_len; 360 | } 361 | } 362 | } 363 | 364 | if (track->synth.fx_delay_amt) { 365 | int delay_shift = (track->synth.fx_delay_time * song->row_len) / 2; 366 | float delay_amount = track->synth.fx_delay_amt / 255.0; 367 | pl_synth_apply_delay(temp_samples, len, delay_shift, delay_amount); 368 | } 369 | 370 | for (int i = 0; i < len_2; i++) { 371 | samples[i] = pl_synth_clamp_s16(samples[i] + (int)temp_samples[i]); 372 | } 373 | } 374 | return len; 375 | } 376 | 377 | 378 | #endif /* PL_SYNTH_IMPLEMENTATION */ 379 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | pl_synth Example 6 | 7 | 8 | 9 | 10 | 11 | 41 | 42 | -------------------------------------------------------------------------------- /js/pl_synth.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2024, Dominic Szablewski - https://phoboslab.org 4 | SPDX-License-Identifier: MIT 5 | 6 | Based on Sonant, published under the Creative Commons Public License 7 | (c) 2008-2009 Jake Taylor [ Ferris / Youth Uprising ] 8 | 9 | */ 10 | 11 | let pl_synth_init = (ctx) => { 12 | let 13 | samplerate = 44100, 14 | 15 | tab_size = 4096, 16 | tab_mask = tab_size-1, 17 | tab = new Float32Array(tab_size * 4), 18 | rand_state = 0xd8f554a5, 19 | 20 | generate = ( 21 | row_len, note, buf_l, buf_r, write_pos = 0, 22 | osc1_oct = 0, osc1_det = 0, osc1_detune = 0, osc1_xenv = 0, osc1_vol = 0, osc1_waveform = 0, 23 | osc2_oct = 0, osc2_det = 0, osc2_detune = 0, osc2_xenv = 0, osc2_vol = 0, osc2_waveform = 0, 24 | noise_fader = 0, 25 | attack = 0, sustain = 0, release = 0, master = 0, 26 | fx_filter = 0, fx_freq = 0, fx_resonance_p = 0, fx_delay_time = 0, fx_delay_amt = 0, fx_pan_freq_p = 0, fx_pan_amt_p = 0, 27 | lfo_osc1_freq = 0, lfo_fx_freq = 0, lfo_freq_p = 0, lfo_amt_p = 0, lfo_waveform = 0 28 | ) => { 29 | let 30 | uint8_norm = 1 / 255, 31 | osc_lfo_offset = lfo_waveform * tab_size, 32 | osc1_offset = osc1_waveform * tab_size, 33 | osc2_offset = osc2_waveform * tab_size, 34 | fx_pan_freq = Math.pow(2, fx_pan_freq_p - 8) / row_len, 35 | fx_pan_amt = fx_pan_amt_p / 512, 36 | fx_osc_m = 0.5 / samplerate * tab_size, 37 | lfo_amt = lfo_amt_p / 512, 38 | lfo_freq = (Math.pow(2, lfo_freq_p - 8) / row_len) * tab_size, 39 | 40 | c1 = 0, 41 | c2 = 0, 42 | 43 | fx_resonance = fx_resonance_p * uint8_norm, 44 | noise_vol = noise_fader * 4.6566e-010, /* 1/(2**31) */ 45 | low = 0, 46 | band = 0, 47 | high = 0, 48 | 49 | inv_attack = 1 / attack, 50 | inv_release = 1 / release, 51 | 52 | osc1_freq = Math.pow(1.059463094, (note + (osc1_oct - 8) * 12 + osc1_det) - 128) * 0.00390625 * (1 + 0.0008 * osc1_detune), 53 | osc2_freq = Math.pow(1.059463094, (note + (osc2_oct - 8) * 12 + osc2_det) - 128) * 0.00390625 * (1 + 0.0008 * osc2_detune), 54 | 55 | num_samples = attack + sustain + release - 1; 56 | 57 | for (let j = num_samples; j >= 0; --j) { 58 | let 59 | k = j + write_pos, 60 | lfor = tab[osc_lfo_offset + ((k * lfo_freq) & tab_mask)] * lfo_amt + 0.5, 61 | 62 | sample = 0, 63 | filter_f = fx_freq, 64 | temp_f, 65 | envelope = 1; 66 | 67 | // Envelope 68 | if (j < attack) { 69 | envelope = j * inv_attack; 70 | } 71 | else if (j >= attack + sustain) { 72 | envelope -= (j - attack - sustain) * inv_release; 73 | } 74 | 75 | // Oscillator 1 76 | temp_f = osc1_freq; 77 | if (lfo_osc1_freq) { 78 | temp_f *= lfor; 79 | } 80 | if (osc1_xenv) { 81 | temp_f *= envelope * envelope; 82 | } 83 | c1 += temp_f; 84 | sample += tab[osc1_offset + ((c1 * tab_size) & tab_mask)] * osc1_vol; 85 | 86 | // Oscillator 2 87 | temp_f = osc2_freq; 88 | if (osc2_xenv) { 89 | temp_f *= envelope * envelope; 90 | } 91 | c2 += temp_f; 92 | sample += tab[osc2_offset + ((c2 * tab_size) & tab_mask)] * osc2_vol; 93 | 94 | // Noise oscillator 95 | if (noise_fader) { 96 | rand_state ^= rand_state << 13; 97 | rand_state ^= rand_state >> 17; 98 | rand_state ^= rand_state << 5; 99 | sample += rand_state * noise_vol * envelope; 100 | } 101 | 102 | sample *= envelope * uint8_norm; 103 | 104 | // State variable filter 105 | if (fx_filter) { 106 | if (lfo_fx_freq) { 107 | filter_f *= lfor; 108 | } 109 | filter_f = 1.5 * tab[(filter_f * fx_osc_m) & tab_mask]; 110 | low += filter_f * band; 111 | high = fx_resonance * (sample - band) - low; 112 | band += filter_f * high; 113 | sample = [sample, high, low, band, low + high][fx_filter]; 114 | } 115 | 116 | // Panning & master volume 117 | temp_f = tab[(k * fx_pan_freq * tab_size) & tab_mask] * fx_pan_amt + 0.5; 118 | sample *= 0.00238 * master; 119 | 120 | buf_l[k] += sample * (1-temp_f); 121 | buf_r[k] += sample * temp_f; 122 | } 123 | }, 124 | 125 | unundefine = (data) => { 126 | for (let i = 0; i < data.length; i++) { 127 | data[i] = Array.isArray(data[i]) ? unundefine(data[i]) : (data[i] ?? 0); 128 | } 129 | return data; 130 | }, 131 | 132 | instrumentLen = (instrument, row_len) => { 133 | let 134 | delay_shift = (instrument[20/*fx_delay_time*/] * row_len) >> 1, 135 | delay_amount = instrument[21/*fx_delay_amt*/] / 255, 136 | delay_iter = Math.ceil(Math.log(0.1) / Math.log(delay_amount)); 137 | return instrument[13/*env_attack*/] + 138 | instrument[14/*env.sustain*/] + 139 | instrument[15/*env.release*/] + 140 | delay_iter * delay_shift; 141 | }, 142 | 143 | apply_delay = (left, right, start, row_len, instrument) => { 144 | if (!instrument[21/*fx_delay_amt*/]) { 145 | return; 146 | } 147 | let 148 | delay_shift = (instrument[20/*fx_delay_time*/] * row_len) >> 1, 149 | delay_amount = instrument[21/*fx_delay_amt*/] / 255, 150 | len = left.length - delay_shift; 151 | for (let i = start, j = start + delay_shift; i < len; i++, j++) { 152 | left[j] += right[i] * delay_amount; 153 | right[j] += left[i] * delay_amount; 154 | } 155 | }, 156 | 157 | sound = (instrument, note = 147 /* C-5 */, row_len = 5513 /* 120 BPM */) => { 158 | instrument = unundefine(instrument); 159 | 160 | let 161 | num_samples = instrumentLen(instrument, row_len), 162 | audio_buffer = ctx.createBuffer(2, num_samples, samplerate), 163 | samples_l = audio_buffer.getChannelData(0), 164 | samples_r = audio_buffer.getChannelData(1); 165 | 166 | generate(row_len, note, samples_l, samples_r, 0, ...instrument); 167 | apply_delay(samples_l, samples_r, 0, row_len, instrument); 168 | return audio_buffer; 169 | }, 170 | 171 | song = (songData) => { 172 | songData = unundefine(songData); 173 | 174 | let 175 | row_len = songData[0/*row_len*/], 176 | tracks = songData[1/*track*/], 177 | num_samples = 0; 178 | for (let track of tracks) { 179 | let track_samples = track[1/*sequence*/].length * row_len * 32 + 180 | instrumentLen(track[0/*instrument*/], row_len); 181 | 182 | if (track_samples > num_samples) { 183 | num_samples = track_samples; 184 | } 185 | } 186 | 187 | let 188 | audio_buffer = ctx.createBuffer(2, num_samples, samplerate), 189 | song_samples_l = audio_buffer.getChannelData(0), 190 | song_samples_r = audio_buffer.getChannelData(1), 191 | track_samples_l = new Float32Array(num_samples), 192 | track_samples_r = new Float32Array(num_samples); 193 | 194 | for (let track of tracks) { 195 | let 196 | instrument = track[0/*instrument*/], 197 | sequence = track[1/*sequence*/], 198 | write_pos = 0, 199 | first = num_samples; 200 | 201 | track_samples_l.fill(0); 202 | track_samples_r.fill(0); 203 | 204 | for (let pi of sequence) { 205 | for (let row = 0; row < 32; row++) { 206 | let note = track[2/*patterns*/][pi-1]?.[row]; 207 | if (note) { 208 | first = Math.min(first, write_pos); 209 | generate(row_len, note, track_samples_l, track_samples_r, write_pos, ...instrument); 210 | } 211 | write_pos += row_len; 212 | } 213 | } 214 | 215 | apply_delay(track_samples_l, track_samples_r, first, row_len, instrument); 216 | 217 | for (let i = first; i < num_samples; i++) { 218 | song_samples_l[i] += track_samples_l[i]; 219 | song_samples_r[i] += track_samples_r[i]; 220 | } 221 | } 222 | return audio_buffer; 223 | }; 224 | 225 | // Generate the lookup tab with 4 oscilators: sin, square, saw, tri 226 | for (let i = 0; i < tab_size; i++) { 227 | tab[i ] = Math.sin(i*6.283184/tab_size); 228 | tab[i + tab_size ] = tab[i] < 0 ? -1 : 1; 229 | tab[i + tab_size * 2] = i / tab_size - 0.5; 230 | tab[i + tab_size * 3] = i < tab_size/2 ? (i/(tab_size/4)) - 1 : 3 - (i/(tab_size/4)); 231 | } 232 | 233 | return {sound, song}; 234 | }; 235 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | CC=clang 2 | TERSER=terser 3 | SED=sed 4 | BASE64=base64 5 | 6 | # Compiler and linker flags 7 | CFLAGS=-O3 8 | WASMFLAGS=--target=wasm32 -nostdlib -Wl,--no-entry -Wl,--export-all -mbulk-memory 9 | LDFLAGS=-lm 10 | 11 | # Source and target files 12 | PLAIN_JS=js/pl_synth.js 13 | PLAIN_JS_MIN=release/pl_synth.min.js 14 | WASM_SRC=wasm/pl_synth_wasm_module.c 15 | WASM_TARGET=wasm/pl_synth.wasm 16 | WASM_TEMPLATE=wasm/pl_synth_wasm_template.js 17 | WASM_JS=wasm/pl_synth_wasm.js 18 | WASM_JS_MIN=release/pl_synth_wasm.min.js 19 | NATIVE_SRC=c/example.c c/pl_synth.h 20 | NATIVE_BIN=c/example 21 | 22 | .PHONY: all clean 23 | 24 | all: $(PLAIN_JS_MIN) $(WASM_JS_MIN) $(NATIVE_BIN) 25 | 26 | # Target: Minified plain JS version of pl_synth 27 | $(PLAIN_JS_MIN): $(PLAIN_JS) 28 | $(TERSER) $< -c -m -o $@ 29 | 30 | # Target: Minified JS + WASM version of pl_synth 31 | $(WASM_JS_MIN): $(WASM_JS) 32 | $(TERSER) $< -c -m -o $@ 33 | 34 | # Embed WASM module into JS 35 | $(WASM_JS): $(WASM_TEMPLATE) $(WASM_TARGET) 36 | $(SED) "s|{{WASM_MODULE_EMBEDDED_HERE_AS_BASE64}}|$(shell $(BASE64) -w 0 $(WASM_TARGET))|" $(WASM_TEMPLATE) > $@ 37 | 38 | # Compile the WASM module 39 | $(WASM_TARGET): $(WASM_SRC) 40 | $(CC) $(CFLAGS) $(WASMFLAGS) -o $@ $< 41 | 42 | # Target: Compiled native example 43 | $(NATIVE_BIN): $(NATIVE_SRC) 44 | $(CC) $< $(CFLAGS) $(LDFLAGS) -o $@ 45 | 46 | # Clean up generated files 47 | clean: 48 | rm -f $(PLAIN_JS_MIN) $(WASM_TARGET) $(WASM_JS) $(WASM_JS_MIN) $(NATIVE_BIN) -------------------------------------------------------------------------------- /release/pl_synth.min.js: -------------------------------------------------------------------------------- 1 | let pl_synth_init=t=>{let e=44100,l=4096,r=4095,a=new Float32Array(16384),n=3639956645,o=(t,e,o,f,h=0,g=0,i=0,u=0,M=0,w=0,y=0,p=0,s=0,A=0,C=0,D=0,c=0,F=0,B=0,_=0,d=0,m=0,b=0,j=0,k=0,q=0,v=0,x=0,z=0,E=0,G=0,H=0,I=0,J=0)=>{let K=1/255,L=J*l,N=y*l,O=c*l,P=Math.pow(2,x-8)/t,Q=z/512,R=I/512,S=Math.pow(2,H-8)/t*l,T=0,U=0,V=k*K,W=4.6566e-10*F,X=0,Y=0,Z=0,$=1/B,tt=1/d,et=.00390625*Math.pow(1.059463094,e+12*(g-8)+i-128)*(1+8e-4*u),lt=.00390625*Math.pow(1.059463094,e+12*(p-8)+s-128)*(1+8e-4*A);for(let t=B+_+d-1;t>=0;--t){let e,g=t+h,i=a[L+(g*S&r)]*R+.5,u=0,y=j,p=1;t=B+_&&(p-=(t-B-_)*tt),e=et,E&&(e*=i),M&&(e*=p*p),T+=e,u+=a[N+(T*l&r)]*w,e=lt,C&&(e*=p*p),U+=e,u+=a[O+(U*l&r)]*D,F&&(n^=n<<13,n^=n>>17,n^=n<<5,u+=n*W*p),u*=p*K,b&&(G&&(y*=i),y=1.5*a[.046439909297052155*y&r],X+=y*Y,Z=V*(u-Y)-X,Y+=y*Z,u=[u,Z,X,Y,X+Z][b]),e=a[g*P*l&r]*Q+.5,u*=.00238*m,o[g]+=u*(1-e),f[g]+=u*e}},f=t=>{for(let e=0;e{let l=t[20]*e>>1,r=t[21]/255,a=Math.ceil(Math.log(.1)/Math.log(r));return t[13]+t[14]+t[15]+a*l},g=(t,e,l,r,a)=>{if(!a[21])return;let n=a[20]*r>>1,o=a[21]/255,f=t.length-n;for(let r=l,a=l+n;r{l=f(l);let n=h(l,a),i=t.createBuffer(2,n,e),u=i.getChannelData(0),M=i.getChannelData(1);return o(a,r,u,M,0,...l),g(u,M,0,a,l),i},song:l=>{let r=(l=f(l))[0],a=l[1],n=0;for(let t of a){let e=t[1].length*r*32+h(t[0],r);e>n&&(n=e)}let i=t.createBuffer(2,n,e),u=i.getChannelData(0),M=i.getChannelData(1),w=new Float32Array(n),y=new Float32Array(n);for(let t of a){let e=t[0],l=t[1],a=0,f=n;w.fill(0),y.fill(0);for(let n of l)for(let l=0;l<32;l++){let h=t[2][n-1]?.[l];h&&(f=Math.min(f,a),o(r,h,w,y,a,...e)),a+=r}g(w,y,f,r,e);for(let t=f;t{let I=null,C=0,t=A=>{let g=C+2*A*4,t=Math.ceil(g/65536),e=Math.ceil(I.memory.buffer.byteLength/65536);return t>e&&I.memory.grow(t-e),[new Float32Array(I.memory.buffer,C,A),new Float32Array(I.memory.buffer,C+4*A,A)]},e=A=>{for(let g=0;g{let I=A[20]*g>>1,C=A[21]/255,t=Math.ceil(Math.log(.1)/Math.log(C));return A[13]+A[14]+A[15]+t*I},E=(A,g,C,t)=>{let e=t[20]*C>>1,Q=t[21]/255;Q&&I.delay(A.byteOffset,g.byteOffset,A.length,e,Q)},f=(g,C=147,f=5513)=>{g=e(g);let B=Q(g,f),i=A.createBuffer(2,B,44100),l=i.getChannelData(0),a=i.getChannelData(1),[h,s]=t(B);return I.clear(h.byteOffset,h.length),I.clear(s.byteOffset,s.length),I.gen(h.byteOffset,s.byteOffset,0,f,C,...g),E(h,s,f,g),l.set(h),a.set(s),i},B=g=>{let C=(g=e(g))[0],f=g[1],B=0;for(let A of f){let g=A[1].length*C*32+Q(A[0],C);g>B&&(B=g)}let i=A.createBuffer(2,B,44100),l=i.getChannelData(0),a=i.getChannelData(1),[h,s]=t(B);for(let A of f){let g=A[0],t=A[1],e=0,Q=B;I.clear(h.byteOffset,h.length),I.clear(s.byteOffset,s.length);for(let E of t)for(let t=0;t<32;t++){let f=A[2][E-1]?.[t];f&&(Q=Math.min(Q,e),I.gen(h.byteOffset,s.byteOffset,e,C,f,...g)),e+=C}E(h,s,C,g);for(let A=Q;A{I=A.instance.exports,I.init(),C=I.memory.buffer.byteLength,g&&g({sound:f,song:B})}))}; -------------------------------------------------------------------------------- /tracker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | pl_synth 6 | 663 | 664 | 665 | 666 | 3535 | 3536 | 3537 | -------------------------------------------------------------------------------- /wasm/pl_synth_wasm_module.c: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2024, Dominic Szablewski - https://phoboslab.org 4 | SPDX-License-Identifier: MIT 5 | 6 | Based on Sonant, published under the Creative Commons Public License 7 | (c) 2008-2009 Jake Taylor [ Ferris / Youth Uprising ] 8 | 9 | */ 10 | 11 | #include 12 | 13 | #define PL_SYNTH_SAMPLERATE 44100 14 | #define PL_SYNTH_TAB_LEN 4096 15 | #define PL_SYNTH_TAB_MASK (PL_SYNTH_TAB_LEN-1) 16 | #define PL_SYNTH_TAB(WAVEFORM, K) tab[WAVEFORM][((int)((K) * PL_SYNTH_TAB_LEN)) & PL_SYNTH_TAB_MASK] 17 | 18 | static uint32_t rand_state = 0xd8f554a5; 19 | static float tab[4][PL_SYNTH_TAB_LEN]; 20 | 21 | extern float __attribute__((import_module("env"), import_name("sin"))) 22 | js_sinf(float v); 23 | 24 | extern float __attribute__((import_module("env"), import_name("pow"))) 25 | js_powf(float base, float exp); 26 | 27 | static inline float note_freq(int n, int oct, int semi, int detune) { 28 | return (0.00390625 * js_powf(1.059463094, n - 128 + (oct - 8) * 12 + semi)) * (1.0f + 0.0008f * detune); 29 | } 30 | 31 | void init() { 32 | for (int i = 0; i < PL_SYNTH_TAB_LEN; i++) { 33 | tab[0][i] = js_sinf(i*(float)(6.283184/PL_SYNTH_TAB_LEN)); 34 | tab[1][i] = tab[0][i] < 0 ? -1 : 1; 35 | tab[2][i] = (float)i / PL_SYNTH_TAB_LEN - 0.5; 36 | tab[3][i] = i < PL_SYNTH_TAB_LEN/2 37 | ? (i/(PL_SYNTH_TAB_LEN/4.0)) - 1.0 38 | : 3.0 - (i/(PL_SYNTH_TAB_LEN/4.0)); 39 | } 40 | } 41 | 42 | void gen( 43 | float *samples_l, 44 | float *samples_r, 45 | int write_pos, 46 | 47 | int row_len, 48 | uint8_t note, 49 | 50 | uint8_t osc0_oct, 51 | uint8_t osc0_det, 52 | uint8_t osc0_detune, 53 | uint8_t osc0_xenv, 54 | uint8_t osc0_vol, 55 | uint8_t osc0_waveform, 56 | 57 | uint8_t osc1_oct, 58 | uint8_t osc1_det, 59 | uint8_t osc1_detune, 60 | uint8_t osc1_xenv, 61 | uint8_t osc1_vol, 62 | uint8_t osc1_waveform, 63 | 64 | uint8_t noise_fader, 65 | 66 | uint32_t env_attack, 67 | uint32_t env_sustain, 68 | uint32_t env_release, 69 | uint32_t env_master, 70 | 71 | uint8_t fx_filter, 72 | uint32_t fx_freq, 73 | uint8_t fx_resonance_p, 74 | uint8_t fx_delay_time, 75 | uint8_t fx_delay_amt, 76 | uint8_t fx_pan_freq_p, 77 | uint8_t fx_pan_amt_p, 78 | 79 | uint8_t lfo_osc0_freq, 80 | uint8_t lfo_fx_freq, 81 | uint8_t lfo_freq_p, 82 | uint8_t lfo_amt_p, 83 | uint8_t lfo_waveform 84 | ) { 85 | float fx_pan_freq = js_powf(2, fx_pan_freq_p - 8) / row_len; 86 | float lfo_freq = js_powf(2, lfo_freq_p - 8) / row_len; 87 | 88 | // We need higher precision here, because the oscilator positions may be 89 | // advanced by tiny values and error accumulates over time 90 | double osc0_pos = 0; 91 | double osc1_pos = 0; 92 | 93 | float fx_resonance = fx_resonance_p / 255.0f; 94 | float noise_vol = noise_fader * 4.6566129e-010f; 95 | float low = 0; 96 | float band = 0; 97 | float high = 0; 98 | 99 | float inv_attack = 1.0f / env_attack; 100 | float inv_release = 1.0f / env_release; 101 | float lfo_amt = lfo_amt_p / 512.0f; 102 | float pan_amt = fx_pan_amt_p / 512.0f; 103 | 104 | float osc0_freq = note_freq(note, osc0_oct, osc0_det, osc0_detune); 105 | float osc1_freq = note_freq(note, osc1_oct, osc1_det, osc1_detune); 106 | 107 | int num_samples = env_attack + env_sustain + env_release - 1; 108 | 109 | for (int j = num_samples; j >= 0; j--) { 110 | int k = j + write_pos; 111 | 112 | // LFO 113 | float lfor = PL_SYNTH_TAB(lfo_waveform, k * lfo_freq) * lfo_amt + 0.5f; 114 | 115 | float sample = 0; 116 | float filter_f = fx_freq; 117 | float temp_f; 118 | float envelope = 1; 119 | 120 | // Envelope 121 | if (j < env_attack) { 122 | envelope = (float)j * inv_attack; 123 | } 124 | else if (j >= env_attack + env_sustain) { 125 | envelope -= (float)(j - env_attack - env_sustain) * inv_release; 126 | } 127 | 128 | // Oscillator 1 129 | temp_f = osc0_freq; 130 | if (lfo_osc0_freq) { 131 | temp_f *= lfor; 132 | } 133 | if (osc0_xenv) { 134 | temp_f *= envelope * envelope; 135 | } 136 | osc0_pos += temp_f; 137 | sample += PL_SYNTH_TAB(osc0_waveform, osc0_pos) * osc0_vol; 138 | 139 | // Oscillator 2 140 | temp_f = osc1_freq; 141 | if (osc1_xenv) { 142 | temp_f *= envelope * envelope; 143 | } 144 | osc1_pos += temp_f; 145 | sample += PL_SYNTH_TAB(osc1_waveform, osc1_pos) * osc1_vol; 146 | 147 | // Noise oscillator 148 | if (noise_vol) { 149 | int32_t r = (int32_t)rand_state; 150 | sample += (float)r * noise_vol * envelope; 151 | rand_state ^= rand_state << 13; 152 | rand_state ^= rand_state >> 17; 153 | rand_state ^= rand_state << 5; 154 | } 155 | 156 | sample *= envelope * (1.0f / 255.0f); 157 | 158 | // State variable filter 159 | if (fx_filter) { 160 | if (lfo_fx_freq) { 161 | filter_f *= lfor; 162 | } 163 | 164 | filter_f = PL_SYNTH_TAB(0, filter_f * (0.5f / PL_SYNTH_SAMPLERATE)) * 1.5f; 165 | low += filter_f * band; 166 | high = fx_resonance * (sample - band) - low; 167 | band += filter_f * high; 168 | sample = (float[5]){sample, high, low, band, low + high}[fx_filter]; 169 | } 170 | 171 | // Panning & master volume 172 | temp_f = PL_SYNTH_TAB(0, k * fx_pan_freq) * pan_amt + 0.5f; 173 | sample *= 0.00238f * env_master; 174 | 175 | samples_l[k] += (sample * (1-temp_f)); 176 | samples_r[k] += (sample * temp_f); 177 | } 178 | } 179 | 180 | void clear(float *samples, int len) { 181 | for (int i = 0; i < len; i++) { 182 | samples[i] = 0; 183 | } 184 | } 185 | 186 | void delay(float *samples_l, float *samples_r, int len, int shift, float amount) { 187 | for (int i = 0, j = shift; j < len; i ++, j ++) { 188 | samples_l[j] += samples_r[i] * amount; 189 | samples_r[j] += samples_l[i] * amount; 190 | } 191 | } -------------------------------------------------------------------------------- /wasm/pl_synth_wasm_template.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2024, Dominic Szablewski - https://phoboslab.org 4 | SPDX-License-Identifier: MIT 5 | 6 | Based on Sonant, published under the Creative Commons Public License 7 | (c) 2008-2009 Jake Taylor [ Ferris / Youth Uprising ] 8 | 9 | */ 10 | 11 | let pl_synth_wasm_init = (ctx, callback) => { 12 | let 13 | samplerate = 44100, 14 | 15 | wasm = null, 16 | wasm_page_size = 64 * 1024, 17 | wasm_mem_base = 0, 18 | wasm_source = '{{WASM_MODULE_EMBEDDED_HERE_AS_BASE64}}', 19 | 20 | alloc = (num_samples) => { 21 | // Ensures the WASM instance has enough memory for backing two channels 22 | // of num_samples of audio data. This "overwrites" previously allocated 23 | // Float32Arrays, if any. 24 | let req_size = wasm_mem_base + num_samples * 2 * 4; 25 | let req_pages = Math.ceil(req_size / wasm_page_size); 26 | let pages = Math.ceil(wasm.memory.buffer.byteLength / wasm_page_size); 27 | if (req_pages > pages) { 28 | wasm.memory.grow(req_pages - pages); 29 | } 30 | return [ 31 | new Float32Array(wasm.memory.buffer, wasm_mem_base, num_samples), 32 | new Float32Array(wasm.memory.buffer, wasm_mem_base + num_samples * 4, num_samples) 33 | ]; 34 | }, 35 | 36 | unundefine = (data) => { 37 | for (let i = 0; i < data.length; i++) { 38 | data[i] = Array.isArray(data[i]) ? unundefine(data[i]) : (data[i] ?? 0); 39 | } 40 | return data; 41 | }, 42 | 43 | instrumentLen = (instrument, row_len) => { 44 | let delay_shift = (instrument[20/*fx_delay_time*/] * row_len) >> 1, 45 | delay_amount = instrument[21/*fx_delay_amt*/] / 255, 46 | delay_iter = Math.ceil(Math.log(0.1) / Math.log(delay_amount)); 47 | return instrument[13/*env_attack*/] + 48 | instrument[14/*env.sustain*/] + 49 | instrument[15/*env.release*/] + 50 | delay_iter * delay_shift; 51 | }, 52 | 53 | apply_delay = (left, right, row_len, instrument) => { 54 | let 55 | delay_shift = (instrument[20/*fx_delay_time*/] * row_len) >> 1, 56 | delay_amount = instrument[21/*fx_delay_amt*/] / 255; 57 | if (delay_amount) { 58 | wasm.delay(left.byteOffset, right.byteOffset, left.length, delay_shift, delay_amount); 59 | } 60 | }, 61 | 62 | sound = (instrument, note = 147 /* C-5 */, row_len = 5513 /* 120 BPM */) => { 63 | instrument = unundefine(instrument); 64 | 65 | let 66 | num_samples = instrumentLen(instrument, row_len), 67 | audio_buffer = ctx.createBuffer(2, num_samples, samplerate), 68 | sound_left = audio_buffer.getChannelData(0), 69 | sound_right = audio_buffer.getChannelData(1), 70 | [left, right] = alloc(num_samples); 71 | 72 | wasm.clear(left.byteOffset, left.length); 73 | wasm.clear(right.byteOffset, right.length); 74 | 75 | wasm.gen(left.byteOffset, right.byteOffset, 0, row_len, note, ...instrument); 76 | apply_delay(left, right, row_len, instrument); 77 | 78 | sound_left.set(left); 79 | sound_right.set(right); 80 | 81 | return audio_buffer; 82 | }, 83 | 84 | song = (songData) => { 85 | songData = unundefine(songData); 86 | 87 | let 88 | row_len = songData[0/*row_len*/], 89 | tracks = songData[1/*track*/], 90 | num_samples = 0; 91 | for (let track of tracks) { 92 | let track_samples = track[1/*sequence*/].length * row_len * 32 + instrumentLen(track[0/*instrument*/], row_len); 93 | if (track_samples > num_samples) { 94 | num_samples = track_samples; 95 | } 96 | } 97 | 98 | let 99 | audio_buffer = ctx.createBuffer(2, num_samples, samplerate), 100 | song_left = audio_buffer.getChannelData(0), 101 | song_right = audio_buffer.getChannelData(1), 102 | [left, right] = alloc(num_samples); 103 | 104 | for (let track of tracks) { 105 | let 106 | instrument = track[0/*instrument*/], 107 | sequence = track[1/*sequence*/], 108 | write_pos = 0, 109 | first = num_samples; 110 | 111 | wasm.clear(left.byteOffset, left.length); 112 | wasm.clear(right.byteOffset, right.length); 113 | 114 | for (let pi of sequence) { 115 | for (let row = 0; row < 32; row++) { 116 | let note = track[2/*patterns*/][pi-1]?.[row]; 117 | if (note) { 118 | first = Math.min(first, write_pos); 119 | wasm.gen(left.byteOffset, right.byteOffset, write_pos, row_len, note, ...instrument); 120 | } 121 | write_pos += row_len; 122 | } 123 | } 124 | 125 | apply_delay(left, right, row_len, instrument); 126 | 127 | for (let i = first; i < num_samples; i++) { 128 | song_left[i] += left[i]; 129 | song_right[i] += right[i]; 130 | } 131 | } 132 | return audio_buffer; 133 | }; 134 | 135 | let wasm_bin = atob(wasm_source); 136 | let wasm_bytes = new Uint8Array(wasm_bin.length); 137 | for (let i = 0; i < wasm_bin.length; i++) { 138 | wasm_bytes[i] = wasm_bin.charCodeAt(i); 139 | } 140 | 141 | WebAssembly.instantiate(wasm_bytes, {env: {pow: Math.pow, sin: Math.sin}}).then(r => { 142 | wasm = r.instance.exports; 143 | wasm.init(); 144 | wasm_mem_base = wasm.memory.buffer.byteLength; 145 | callback && callback({sound, song}); 146 | }); 147 | }; 148 | --------------------------------------------------------------------------------