├── 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 |
--------------------------------------------------------------------------------