├── .gitignore ├── WebAudio.png ├── addon.json ├── .github ├── workflows │ ├── linter.yml │ └── deploy.yml ├── PULL_REQUEST_TEMPLATE │ ├── whitelist_addition.md │ └── basic.md └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE.md ├── definitions.lua ├── CONTRIBUTING.md ├── .glualint.json ├── examples └── Radio.txt ├── WHITELIST.md ├── STEAM_README.txt ├── lua ├── entities │ └── gmod_wire_expression2 │ │ └── core │ │ └── custom │ │ ├── cl_webaudio.lua │ │ └── webaudio.lua ├── autorun │ ├── stopwatch.lua │ └── webaudio.lua └── webaudio │ ├── receiver.lua │ └── interface.lua └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .vs 3 | /readme_tables 4 | webaudio.gma -------------------------------------------------------------------------------- /WebAudio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thevurv/WebAudio/HEAD/WebAudio.png -------------------------------------------------------------------------------- /addon.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "WebAudio", 3 | "type": "tool", 4 | "tags": ["build", "fun"], 5 | "description": "An addon that adds a WebAudio class to allow for server sync with client IGmodAudioChannels to make efficient & safe E2 Media URL Streams", 6 | "author": "Vurv" 7 | } -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'lua/**' 7 | pull_request: 8 | paths: 9 | - 'lua/**' 10 | workflow_dispatch: 11 | 12 | 13 | jobs: 14 | lint: 15 | uses: FPtje/GLuaFixer/.github/workflows/glualint.yml@master -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/whitelist_addition.md: -------------------------------------------------------------------------------- 1 | ## Domain 2 | _Please put the domain you wish to whitelist here_ 3 | 4 | ## Necessary info 5 | Here's some stuff you'll also need to give 6 | 1. Is the website by a large company? Or do you have proof that it is from a reliable source? 7 | 2. Example link to the website (if possible, you can also just replace personal stuff with x's or something.) 8 | 9 | ## Notes 10 | Remember to both add it to ``lua/autorun/webaudio.lua`` AND ``WHITELIST.md`` -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Workshop 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@master 15 | - uses: vurv78/gmod-upload@master 16 | with: 17 | id: 2466875474 18 | changelog: "${{ github.event.head_commit.message }}" 19 | env: 20 | STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }} 21 | STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }} 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Join local server 16 | 2. Do '...' 17 | 3. Then do '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Windows] 28 | - Gmod Branch: [e.g. dev] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vurv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /definitions.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable:lowercase-global 2 | 3 | ---@type boolean 4 | SERVER = nil 5 | 6 | ---@type boolean 7 | CLIENT = nil 8 | 9 | E2Helper = {} 10 | E2Lib = {} 11 | 12 | wire_expression2_funcs = {} 13 | 14 | ---@param msg string 15 | ---@param level integer? 16 | ---@param trace table 17 | ---@param can_catch boolean? Default true 18 | function E2Lib.raiseException(msg, level, trace, can_catch) 19 | end 20 | 21 | ---@param name string 22 | ---@param value number 23 | function E2Lib.registerConstant(name, value, literal) 24 | 25 | end 26 | 27 | ---@param num integer 28 | function __e2setcost(num) end 29 | 30 | ---@param name string 31 | ---@param id string 32 | ---@param def any 33 | function registerType(name, id, def, ...) 34 | 35 | end 36 | 37 | ---@param name string name of the function 38 | ---@param pars string params 39 | ---@param rets string ret 40 | ---@param func function 41 | ---@param cost number? 42 | ---@param argnames table? 43 | function registerOperator(name, pars, rets, func, cost, argnames) 44 | 45 | end 46 | 47 | 48 | registerFunction = registerOperator 49 | 50 | ---@param name string 51 | ---@param cb function 52 | function registerCallback(name, cb) end -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Wanna add something to the addon? Here's some rules and steps for doing so. 3 | 4 | ## Checks 5 | 1. Make sure what you want to add doesn't already exist / has been fixed / has been assigned to someone else. 6 | 1. Make an issue before trying to pr 7 | 2. Make sure you also document the stuff you're adding. 8 | 1. Steam README 9 | 2. Github README.md 10 | 3. cl_webaudio.lua 11 | 12 | ## Rules 13 | 1. Do *NOT* use glua specific / garry syntax. Talking about !=, !, continue. (Yes, even continue.) 14 | 2. Make sure to comply with the linter. 15 | 1. This means making sure there's no trailing whitespace. 16 | 2. Having spaces after operators 17 | 3. Avoiding use of globals, etc.. 18 | 19 | ## Recommendations 20 | Use [Glua Enhanced](https://marketplace.visualstudio.com/items?itemName=venner.vscode-glua-enhanced) (Autocompletion, Syntax Highlighting, etc..) 21 | And [vscode-glualint](https://marketplace.visualstudio.com/items?itemName=goz3rr.vscode-glualint) (VSCode version of the linter) 22 | 23 | ## Note 24 | Changes may not be accepted depending on whether: 25 | 1. The code isn't efficient 26 | 2. I would rather implement it myself 27 | 3. It has a controversial or breaking change -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/basic.md: -------------------------------------------------------------------------------- 1 | Note that this is just copied from ``CONTRIBUTING.md`` and might be outdated. 2 | 3 | ## Checks 4 | 1. Make sure what you want to add doesn't already exist / has been fixed / has been assigned to someone else. 5 | 1. Make an issue before trying to pr 6 | 2. Make sure you also document the stuff you're adding. 7 | 1. Steam README 8 | 2. Github README.md 9 | 3. cl_webaudio.lua 10 | 11 | ## Rules 12 | 1. Do *NOT* use glua specific / garry syntax. Talking about !=, !, continue. (Yes, even continue.) 13 | 2. Make sure to comply with the linter. 14 | 1. This means making sure there's no trailing whitespace. 15 | 2. Having spaces after operators 16 | 3. Avoiding use of globals, etc.. 17 | 18 | ## Recommendations 19 | Use [Glua Enhanced](https://marketplace.visualstudio.com/items?itemName=venner.vscode-glua-enhanced) (Autocompletion, Syntax Highlighting, etc..) 20 | And [vscode-glualint](https://marketplace.visualstudio.com/items?itemName=goz3rr.vscode-glualint) (VSCode version of the linter) 21 | 22 | ## Note 23 | Changes may not be accepted depending on whether: 24 | 1. The code isn't efficient 25 | 2. I would rather implement it myself 26 | 3. It has a controversial or breaking change -------------------------------------------------------------------------------- /.glualint.json: -------------------------------------------------------------------------------- 1 | { 2 | "lint_maxScopeDepth": 7, 3 | "lint_syntaxErrors": true, 4 | "lint_syntaxInconsistencies": true, 5 | "lint_deprecated": true, 6 | "lint_trailingWhitespace": true, 7 | "lint_whitespaceStyle": true, 8 | "lint_beginnerMistakes": false, 9 | "lint_emptyBlocks": true, 10 | "lint_shadowing": true, 11 | "lint_gotos": true, 12 | "lint_doubleNegations": true, 13 | "lint_redundantIfStatements": true, 14 | "lint_redundantParentheses": false, 15 | "lint_duplicateTableKeys": true, 16 | "lint_profanity": true, 17 | "lint_unusedVars": true, 18 | "lint_unusedParameters": false, 19 | "lint_unusedLoopVars": false, 20 | "lint_inconsistentVariableStyle": false, 21 | "lint_ignoreFiles": [], 22 | 23 | "prettyprint_spaceAfterParens": false, 24 | "prettyprint_spaceAfterBrackets": false, 25 | "prettyprint_spaceAfterBraces": false, 26 | "prettyprint_spaceAfterLabel": false, 27 | "prettyprint_spaceBeforeComma": false, 28 | "prettyprint_spaceAfterComma": true, 29 | "prettyprint_semicolons": false, 30 | "prettyprint_cStyle": false, 31 | "prettyprint_rejectInvalidCode": false, 32 | "prettyprint_indentation": " ", 33 | "log_format": "auto" 34 | } -------------------------------------------------------------------------------- /examples/Radio.txt: -------------------------------------------------------------------------------- 1 | @name WebAudio Radio 2 | @model models/props_lab/citizenradio.mdl 3 | #Author Vurv 4 | 5 | # Note this will use a lot of ops since it runs on tick instead of every _WA_FFT_DELAY. 6 | # You can easily make it keep track of if it's playing with a persistent global var. 7 | 8 | @persist Prefixes:table Stream:webaudio 9 | 10 | if( chatClk() ) { 11 | local LS = lastSaid() 12 | if(Prefixes[LS[1],number]) { 13 | local Args = LS:explode(" ") 14 | local Cmd = Args:removeString(1):sub(2) 15 | 16 | switch (Cmd) { 17 | case "start", 18 | if( Stream:isValid() ) { 19 | print("Stream already exists!") 20 | } else { 21 | local URL = Args:removeString(1) 22 | if ( webAudioCanCreate(URL) ) { 23 | Stream = webAudio(URL) 24 | Stream:setParent(entity()) 25 | Stream:setRadius(5000) 26 | Stream:play() 27 | 28 | runOnTick(1) 29 | } else { 30 | print("Bad URL / Audio cooldown!") 31 | } 32 | } 33 | break, 34 | 35 | case "volume", 36 | local Vol = Args:removeString(1):toNumber() 37 | Stream:setVolume(Vol) 38 | break, 39 | 40 | case "speed", 41 | local Speed = Args:removeString(1):toNumber() 42 | 43 | Stream:setPlaybackRate(Speed) 44 | break, 45 | 46 | case "stop", 47 | Stream:destroy() 48 | runOnTick(0) 49 | break, 50 | 51 | case "play", 52 | Stream:play() 53 | runOnTick(1) 54 | break, 55 | 56 | case "pause", 57 | Stream:pause() 58 | runOnTick(0) 59 | break 60 | } 61 | } 62 | } elseif( tickClk() ) { 63 | local FFT = Stream:getFFT() 64 | for (I = 1, _WA_FFT_SAMPLES) { 65 | holoScaleUnits(I, vec(0.5, 0.5, FFT[I, number]/4) ) 66 | } 67 | }elseif ( first() ) { 68 | runOnChat(1) 69 | Prefixes = table( 70 | "!" = 1, 71 | "?" = 1 72 | ) 73 | 74 | local E = entity() 75 | for (I = 1, _WA_FFT_SAMPLES) { 76 | holoCreate(I, E:toWorld(vec(1, 20+I, 10)), vec(0.03, 0.01, 0.01) ) 77 | holoParent(I, E) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /WHITELIST.md: -------------------------------------------------------------------------------- 1 | ## Whitelist 2 | This is the default whitelist that webaudio will abide by unless a ``webaudio_whitelist.txt`` file is specified in your data folder. 3 | 4 | ### Key 5 | 🖼️ - This domain might be removed because it is probably an image host and not suitable for audio 6 | ❔ - This domain might be removed for being a personal domain (Especially if this ends up getting merged into wiremod) 7 | ✔️ - This domain will remain whitelisted. 8 | ➖ - Nobody has really vouched for this and I don't know about it but it seems fine 9 | 10 | | Name | Status | Domain | Description | Example URL | 11 | | --- | --- | --- | --- | --- | 12 | | Soundcloud | ✔️ | sndcdn.com | Soundcloud api | 🚧 | 13 | | Google Translate | ✔️ | translate.google.com | Google translate api, can be used for tts | 🚧 | 14 | | Discord | ❌ | cdn.discordapp.com | Discord file uploads, you can post an mp3 and get the link to use it. Disabled because discord blocked steam http headers :\\. | https://cdn.discordapp.com/attachments/732861600708690010/866579835706015765/Sound_Chimera.mp3 | 🚧 | 15 | | Reddit | 🖼️ | i.redditmedia.com | Reddit media uploads. Think this is for images so might be removed. | 🚧 | 16 | | Reddit | 🖼️ | i.redd.it | Reddit uploads | 🚧 | 17 | | Reddit | 🖼️ | preview.redd.it | Reddit uploads. | 🚧 | 18 | | Shoutcast | ✔️ | yp.shoutcast.com | Shoutcast | 🚧 | 19 | | Dropbox | ✔️ | dl.dropboxusercontent.com | Dropbox uploads | 🚧 | 20 | | Dropbox | ✔️ | dropbox.com | Dropbox uploads | 🚧 | 21 | | Dropbox | ✔️ | dl.dropbox.com | Dropbox uploads | 🚧 | 22 | | Github | ✔️ | raw.githubusercontent.com | Github user raw files | 🚧 | 23 | | Github | ✔️ | gist.githubusercontent.com | Github gist hosting | 🚧 | 24 | | Github | ✔️ | raw.github.com | Github raw files | 🚧 | 25 | | Github | ✔️ | cloud.githubusercontent.com | Github user content? | 🚧 | 26 | | Steam | 🖼️ | steamuserimages-a.akamaihd.net | Steam user images | 🚧 | 27 | | Steam | ➖ | steamcdn-a.akamaihd.net | Steam content? | 🚧 | 28 | | Gitlab | ✔️ | gitlab.com | Gitlab content | 🚧 | 29 | | Onedrive | ➖ | onedrive.live.com | Onedrive content | 🚧 | 30 | | Mattjeanes ytdl | ❔ | youtubedl.mattjeanes.com | Ytdl host by mattjeanes https://github.com/MattJeanes/YouTubeDL | 🚧 | 31 | | MyInstants | ✔️ | myinstants.com | Sound effects | https://myinstants.com/media/sounds/taco-bell-bong-sfx.mp3 | 32 | | Moonbase Alpha TTS | ✔️ | tts.cyzon.us | TTS almost identical to moonbase alpha's | https://tts.cyzon.us/tts?text=bruh | 33 | | LiveATC | ➖ | liveatc.net | US Air Traffic Control radio host | https://www.liveatc.net/hlisten.php?mount=lszh1_app_east&icao=lszh | 34 | | TrekCore | ➖ | trekcore.com | Star Trek Sounds | https://trekcore.com/audio/aliensounds/alien_door01.mp3 | 35 | | Broadcastify | ➖ | broadcastify.cdnstream1.com | Radio broadcasts | https://broadcastify.cdnstream1.com/18962 | 36 | | Google Drive | ➖ | drive.google.com/uc | File hosting from google | 🚧 | 37 | | Google Drive | ➖ | docs.google.com/uc | File hosting from google | 🚧 | 38 | | Bandcamp | ➖ | bcbits.com | Bandcamp hosting | 🚧 | 39 | | Google Video | ➖ | googlevideo.com | Video hosting used by YouTube | 🚧 | 40 | -------------------------------------------------------------------------------- /STEAM_README.txt: -------------------------------------------------------------------------------- 1 | [h1]WebAudio[/h1] 2 | [url=https://github.com/Vurv78/WebAudio/wiki]Wiki[/url] 3 | This addon adds a serverside [b]WebAudio[/b] class which can be used to interact with clientside [b]IGmodAudioChannels[/b] asynchronously. 4 | It also adds an extension for Expression2 to interact with these in a safe manner. 5 | 6 | [i]This is essentially a safe and more robust replacement for the popular Streamcore addon, since it is [b]much[/b] more secure and customizable.[/i] 7 | 8 | [h2]Why use this instead of Streamcore?[/h2] 9 | [h3]Streamcore is a dangerous addon.[/h3] 10 | It has no whitelist, meaning [b] anyone can force everyone on the server to download ILLEGAL CONTENT or grab everyone's IP Addresses through links like grabify.[/b] 11 | Server owners aren't safe either, streamcore easily allows you to crash everyone on it through spamming net messages. 12 | 13 | [b][i]In fact, this addon was made out of frustration over how low effort / terrible streamcore is and how the author does not care about the security of people's games whatsoever after years of reports over these vulnerabilities.[/i][/b] 14 | 15 | [i]WebAudio easily solves this through a whitelist, active development and flexible but strong limits.[/i] 16 | 17 | [h3] WebAudio has much more features. [/h3] 18 | Have you ever wanted to pause a stream while it was playing? 19 | Maybe turn up the volume higher because it was too quiet? 20 | Maybe even getting the current playback time or FFT values of the stream? 21 | [url=https://github.com/Vurv78/WebAudio/wiki/Features]You can easily do all of this + much more with WebAudio.[/url] 22 | 23 | [h2]Features[/h2] 24 | [list] 25 | [*] Client and Serverside URL whitelists, both are customizable with simple patterns or more advanced lua patterns. 26 | [*] The [b]WebAudio[/b] type in Expression 2 that adds the ability to change sounds in a 3D space, or change it's volume, position and time whilst the music is playing. 27 | [*] Easy to use Lua api that tries to mirror the [b]IGModAudioChannel[/b] type.[/list] 28 | [*] Find the rest here https://github.com/Vurv78/WebAudio/wiki/Features 29 | [/list] 30 | 31 | [h2]I want to contribute![/h2] 32 | Then use the Github page! [url=https://github.com/Vurv78/WebAudio]https://github.com/Vurv78/WebAudio[/url] 33 | Pull request anything you like, just make sure to see [b]CONTRIBUTING.md[/b] 34 | 35 | [h2]I've found a bug![/h2] 36 | You can report bugs by either posting a report on the [url=https://github.com/Vurv78/WebAudio]Github page[/url], or by starting a new conversation on this page. 37 | Make sure to be [u]detailed and concise[/u] in your bug reports, so that they can be serviced easier! 38 | 39 | [h2]ConVars[/h2] 40 | This is a list of [b]ConVars[/b] that you can change to configure the addon to your liking. 41 | [table] 42 | [tr] 43 | [th]Realm[/th] 44 | [th]Name[/th] 45 | [th]Default Value[/th] 46 | [th]Description[/th] 47 | [/tr] 48 | [tr] 49 | [td]SHARED[/td] 50 | [td]wa_enable[/td] 51 | [td]1[/td] 52 | [td]Shared convar that allows you to disable WebAudio for the server or yourself depending on whether it is executed in the client or server console[/td] 53 | [/tr] 54 | [tr] 55 | [td]SERVER[/td] 56 | [td]wa_admin_only[/td] 57 | [td]0[/td] 58 | [td]Allows you to set WebAudio E2 access to only Admins or Only Super Admins. (0 for Everyone, 1 for Admins, 2 for Super Admins).[/td] 59 | [/tr] 60 | [tr] 61 | [td]SHARED[/td] 62 | [td]wa_volume_max[/td] 63 | [td]300[/td] 64 | [td] 65 | Shared convar that allows you to set the maximum volume a WebAudio stream can play at. 66 | 100 is 100%, 50 is 50% and so on. 67 | Helps to prevent nasty earrape music being played too loudly 68 | [/td] 69 | [/tr] 70 | [tr] 71 | [td]SERVER[/td] 72 | [td]wa_stream_max[/td] 73 | [td]5[/td] 74 | [td]Serverside convar that allows you to set the max amount of streams a player can have at once[/td] 75 | [/tr] 76 | [tr] 77 | [td]SHARED[/td] 78 | [td]wa_radius_max[/td] 79 | [td]3000[/td] 80 | [td]Allows you to set the maximum distance a stream can be heard from. Works on your client.[/td] 81 | [/tr] 82 | [tr] 83 | [td]SHARED[/td] 84 | [td]wa_fft_enable[/td] 85 | [td]1[/td] 86 | [td]Whether FFT data is enabled for the server / your client. You shouldn't need to disable it as it is very lightweight[/td] 87 | [/tr] 88 | [tr] 89 | [td]CLIENT[/td] 90 | [td]wa_verbosity[/td] 91 | [td]1[/td] 92 | [td]Verbosity of console notifications. 2 => URL/Logging + Extra Info, 1 => Only warnings/errors, 0 => Nothing (Avoid this)[/td] 93 | [/tr] 94 | [tr] 95 | [td]SERVER[/td] 96 | [td]wa_sc_compat[/td] 97 | [td]0[/td] 98 | [td]Whether streamcore-compatible functions should be generated for E2 (for backwards compat)[/td] 99 | [/tr] 100 | [/table] 101 | 102 | [h2]Console Commands[/h2] 103 | This is a list of Concommands to use with this addon as a server owner and a user. 104 | 105 | [table] 106 | [tr] 107 | [th]Realm[/th] 108 | [th]Name[/th] 109 | [th]Description[/th] 110 | [/tr] 111 | [tr] 112 | [td]SHARED[/td] 113 | [td]wa_purge[/td] 114 | [td]Purges all currently running streams and makes sure you don't get any useless net messages from them.[/td] 115 | [/tr] 116 | [tr] 117 | [td]SHARED[/td] 118 | [td]wa_reload_whitelist[/td] 119 | [td]Reloads your whitelist at data/webaudio_whitelist.txt[/td] 120 | [/tr] 121 | [tr] 122 | [td]SHARED[/td] 123 | [td]wa_list[/td] 124 | [td]Prints a list of currently playing WebAudio streams (As long as their owner IsValid) with their url, id & owner[/td] 125 | [/tr] 126 | [tr] 127 | [td]SHARED[/td] 128 | [td]wa_help[/td] 129 | [td]Prints the link to the github to your console[/td] 130 | [/tr] 131 | [tr] 132 | [td]CLIENT[/td] 133 | [td]wa_display[/td] 134 | [td]Displays active WebAudio streams to your screen w/ steamid and stream id[/td] 135 | [/tr] 136 | [/table] 137 | 138 | [h2]Function Docs[/h2] 139 | Function documentation has moved to only be on [url=https://github.com/Vurv78/WebAudio]Github[/url]. -------------------------------------------------------------------------------- /lua/entities/gmod_wire_expression2/core/custom/cl_webaudio.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | local tbl = E2Helper.Descriptions 4 | local function desc(name, description) 5 | tbl[name] = description 6 | end 7 | 8 | --#region Base 9 | desc("webAudio(s)", "Returns a WebAudio object of that URL as long as it is whitelisted by the server. Has a 500 ms cooldown between calls. If you can't create a webAudio object, will error, so check webAudioCanCreate before calling this!") 10 | desc("webAudiosLeft()", "Returns the number of WebAudio objects you can create in total for your player") 11 | desc("webAudioCanCreate()", "Returns whether you can create a WebAudio object. Checks cooldown and whether you have another slot for a webaudio stream left.") 12 | desc("webAudioCanCreate(s)", "Same as webAudioCanCreate() but also checks if a URL is whitelisted on the server to use.") 13 | --#endregion 14 | 15 | --#region Misc 16 | desc("nowebaudio()", "Returns an invalid webaudio object") 17 | desc("toString(xwa)", "Returns the something like ``WebAudio []`` with being the ID of the stream") 18 | desc("toString(xwa:)", "Returns the something like ``WebAudio []`` with being the ID of the stream") 19 | --#endregion 20 | 21 | --#region Permissions 22 | desc("webAudioEnabled()", "Returns whether WebAudio playing is enabled on the server for E2") 23 | desc("webAudioAdminOnly()", "Returns whether WebAudio playing is restricted to admins or super admins. 0 for everyone, 1 for admins, 2 for superadmins") 24 | --#endregion 25 | 26 | --#region Special Methods 27 | desc("update(xwa:)", "Sends all of the information of the object given by functions like setPos and setTime to the client. You need to call this after running functions without running ``:play()`` or ``:pause()`` on them since those sync with the client. Returns 1 if could update, 0 if hit transmission quota") 28 | desc("destroy(xwa:)", "Destroys the WebAudio object, rendering it useless. It will silently fail when trying to use it from here on! This gives you another slot to make a WebAudio object. Check if an object is destroyed with isValid!") 29 | desc("pause(xwa:)", "Pauses the stream where it is currently playing, Returns 1 or 0 if could successfully do so, because of quota. Automatically calls updates self internally.") 30 | desc("play(xwa:)", "Starts the stream or continues where it left off after pausing, Returns 1 or 0 if could successfully do so from quota. Automatically updates self internally.") 31 | --#endregion 32 | 33 | --#region Modify Object 34 | desc("setDirection(wxa:v)", "Sets the direction in which the WebAudio stream is playing towards without updating it. Remember to call ``self:update()`` when you are done modifying the object!") 35 | desc("setPos(xwa:v)", "Sets the 3D Position of the WebAudio stream without updating it. Remember to call ``self:update()`` when you are done modifying the object!") 36 | desc("setVolume(xwa:n)", "Sets the volume of the WebAudio stream, in percent format. 200 is 2x as loud as normal, 100 is default. Will error if it is past ``wa_volume_max``") 37 | desc("setTime(xwa:n)", "Sets the time in which the WebAudio stream should seek to in playing the URL.") 38 | desc("setPlaybackRate(xwa:n)", "Sets the playback speed of a webaudio stream. 2 is twice as fast, 0.5 being half, etc.") 39 | desc("setParent(xwa:e)", "Parents the stream position to e, local to the entity. If you've never set the position before, will be parented to the center of the prop. Returns 1 if successfully parented or 0 if prop wasn't valid") 40 | desc("setParent(xwa:)", "Unparents the stream") 41 | desc("setRadius(xwa:n)", "Sets the radius in which to the stream will be heard in. Default is 200 and (default) max is 1500.") 42 | desc("setLooping(xwa:n)", "If n is not 0, sets the stream to loop. Else stops looping.") 43 | desc("set3DEnabled(xwa:n)", "If n is not 0, sets the stream to be 3D. By default streams are 3D. Else, sets the audio to play directly on clients (mono audio).") 44 | --#endregion 45 | 46 | --#region is* Getters 47 | desc("isValid(xwa:)", "Returns 1 or 0 for whether the webaudio object is valid (If it is not destroyed & Not invalid from quota)") 48 | desc("isParented(xwa:)", "Returns 1 or 0 for whether the webaudio object is parented or not. Note that if the stream is parented, you cannot set it's position!") 49 | --#endregion 50 | 51 | --#region Getters 52 | desc("getPos(xwa:)", "Returns the current position of the WebAudio object. This does not work with parenting.") 53 | desc("getVolume(xwa:)", "Returns the volume of the WebAudio object set by setVolume") 54 | desc("getRadius(xwa:)", "Returns the radius of the WebAudio object set by setRadius") 55 | desc("getLooping(xwa:)", "Returns if the stream is looping, set by setLooping") 56 | desc("get3DEnabled(xwa:)", "Returns if the stream's 3D is enabled, set by set3DEnabled") 57 | --#endregion 58 | 59 | --#region Replicated Clientside behavior on server 60 | desc("getTime(xwa:)", "Returns the playback time of the stream in seconds.") 61 | desc("getState(xwa:)", "Returns the state of the stream. 0 is Stopped, 1 is Playing, 2 is Paused. See the CHANNEL_* constants") 62 | --#endregion 63 | 64 | --#region Info received from client 65 | desc("getLength(xwa:)", "Returns the playback duration of the stream in seconds. Will return -1 if couldn't get length.") 66 | desc("getFileName(xwa:)", "Returns the file name of the WebAudio stream. Will usually be the URL you give, but not always. Will return \"\" if we haven't received the name yet.") 67 | desc("getFFT(xwa:)", "Returns the fast fourier transform of the webaudio stream. It is an array of 64 numbers or less, ranging from 0 to 255") 68 | --#endregion 69 | 70 | --#region Aliases 71 | desc("unparent(xwa:)", "Alias of xwa:setParent(), Unparents the stream") 72 | desc("parentTo(xwa:e)", "Alias of xwa:setParent(), Parents the stream to entity e") 73 | --#endregion 74 | 75 | --#region Ignore System 76 | desc("setIgnored(xwa:en)", "If n is not 0, blocks the given user from hearing the stream. Else, unblocks the user.") 77 | desc("setIgnored(xwa:rn)", "If n is not 0, sets the stream to ignore certain players. Else, unblocks them.") 78 | desc("getIgnored(xwa:e)", "Returns if the user is blocked from hearing the stream. Note this also counts if they purged the webaudio stream themself, but doesn't count if they have webaudio disabled. Set by setIgnored") 79 | --#endregion -------------------------------------------------------------------------------- /lua/autorun/stopwatch.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | StopWatch/TimerEx Library 3 | Author: Vurv 4 | This is an advanced version of gmod's timer library that allows for:: 5 | * Speeding up and Slowing down timers 6 | * Looping them 7 | * Getting & Setting the current playback time 8 | * Pausing/Playing/Restarting them. 9 | All in a pretty tiny file. 10 | ]] 11 | 12 | STOPWATCH_STOPPED, STOPWATCH_PLAYING, STOPWATCH_PAUSED = 0, 1, 2 13 | 14 | ---@class Stopwatch 15 | ---@field playback_rate number 16 | ---@field playback_now number 17 | ---@field playback_elapsed number 18 | ---@field playback_duration number 19 | ---@field timerid string # Mangled string id for the stopwatch. Created from string.format("stopwatch_%p", self) (Using self's pointer.) 20 | ---@field state number # STOPWATCH_STOPPED, STOPWATCH_PLAYING, STOPWATCH_PAUSED 21 | ---@field delay number 22 | ---@field looping boolean 23 | local Stopwatch = {} 24 | Stopwatch.__index = Stopwatch 25 | 26 | local timer_now = RealTime 27 | 28 | --- Creates a StopWatch. 29 | ---@param duration number # How long the stopwatch will last 30 | ---@param callback fun(self: Stopwatch) # What to run when the stopwatch finishes. 31 | ---@return Stopwatch 32 | function Stopwatch.new(duration, callback) 33 | assert(duration, "bad argument #1 to 'Stopwatch.new' (number expected)") 34 | assert(callback, "bad argument #2 to 'Stopwatch.new' (function expected)") 35 | 36 | local self = setmetatable({}, Stopwatch) 37 | self.playback_rate = 1 38 | self.playback_now = timer_now() 39 | self.playback_elapsed = 0 40 | self.playback_duration = duration 41 | 42 | self.delay = duration -- Internal timer delay used in timer.Adjust. 43 | self.state = STOPWATCH_STOPPED 44 | 45 | local mangled = string.format("stopwatch_%p", self) 46 | timer.Create(mangled, duration, 1, function() 47 | if self.looping then 48 | -- Reset all stats and configure to current playback rate. 49 | self.playback_now = timer_now() 50 | self.playback_elapsed = 0 51 | self.playback_duration = duration 52 | self.delay = duration 53 | self:SetRate(self.playback_rate) 54 | else 55 | -- Same behavior as :Stop but we already know we hit the end of the timer, so set elapsed to duration. 56 | self.playback_elapsed = self.playback_duration 57 | self.state = STOPWATCH_STOPPED 58 | end 59 | callback(self) 60 | end) 61 | timer.Stop(mangled) 62 | 63 | self.timerid = mangled 64 | 65 | return self 66 | end 67 | 68 | -- Backwards compatibility with __call version 69 | setmetatable(Stopwatch, { 70 | __call = function(self, ...) 71 | return self.new(...) 72 | end 73 | }) 74 | 75 | --- Pauses a stopwatch at the current time to be resumed with :Play 76 | function Stopwatch:Pause() 77 | if self.state == STOPWATCH_PLAYING then 78 | self.state = STOPWATCH_PAUSED 79 | timer.Pause(self.timerid) 80 | end 81 | end 82 | 83 | --- Resumes the stopwatch after it was paused. You can't :Play a :Stop(ped) timer, use :Start for that. 84 | function Stopwatch:Play() 85 | if self.state == STOPWATCH_PAUSED then 86 | self.playback_now = timer_now() 87 | self.state = STOPWATCH_PLAYING 88 | timer.UnPause(self.timerid) 89 | end 90 | end 91 | 92 | --- Used internally by GetTime, don't use. 93 | function Stopwatch:UpdateTime() 94 | if self.state == STOPWATCH_PLAYING then 95 | local now = timer_now() 96 | local elapsed = (now - self.playback_now) * self.playback_rate 97 | 98 | self.playback_elapsed = self.playback_elapsed + elapsed 99 | self.playback_now = now 100 | end 101 | end 102 | 103 | --- Stops the timer with the stored elapsed time. 104 | -- Continue from here with :Start() 105 | function Stopwatch:Stop() 106 | if self.state ~= STOPWATCH_STOPPED then 107 | self:UpdateTime() 108 | self.state = STOPWATCH_STOPPED 109 | timer.Stop(self.timerid) 110 | end 111 | end 112 | 113 | --- (Re)starts the stopwatch. 114 | function Stopwatch:Start() 115 | if self.state == STOPWATCH_STOPPED then 116 | self.playback_now = timer_now() 117 | self.state = STOPWATCH_PLAYING 118 | timer.Start(self.timerid) 119 | end 120 | return self 121 | end 122 | 123 | --- Returns the playback duration of the stopwatch. 124 | ---@return number length 125 | function Stopwatch:GetDuration() 126 | return self.playback_duration 127 | end 128 | 129 | --- Returns the playback duration of the stopwatch. 130 | ---@param duration number 131 | ---@return Stopwatch self 132 | function Stopwatch:SetDuration(duration) 133 | self.playback_duration = duration 134 | self.delay = duration 135 | timer.Adjust( self.timerid, duration, nil, nil ) 136 | if self.playback_elapsed > duration then 137 | self:Stop() 138 | end 139 | return self 140 | end 141 | 142 | --- Sets the playback rate / speed of the stopwatch. 2 is twice as fast, etc. 143 | ---@param speed number 144 | ---@return Stopwatch self 145 | function Stopwatch:SetRate(speed) 146 | self:UpdateTime() 147 | self.playback_rate = speed 148 | -- New Duration - Elapsed 149 | self.delay = (self.playback_duration / speed) - self.playback_elapsed 150 | timer.Adjust( self.timerid, self.delay, nil, nil ) 151 | return self 152 | end 153 | 154 | --- Returns the playback rate of the stopwatch. Default 1 155 | ---@return number rate # Playback rate 156 | function Stopwatch:GetRate() 157 | return self.playback_rate 158 | end 159 | 160 | --- Sets the playback time of the stopwatch. 161 | ---@param n number # Time 162 | ---@return Stopwatch self 163 | function Stopwatch:SetTime(n) 164 | self.playback_now = timer_now() 165 | self.playback_elapsed = n 166 | self.delay = (self.playback_duration - n) / self.playback_rate 167 | 168 | timer.Adjust( self.timerid, self.delay, nil, nil ) 169 | return self 170 | end 171 | 172 | --- Returns the current playback time in seconds of the stopwatch 173 | ---@return number time # Playback time 174 | function Stopwatch:GetTime() 175 | self:UpdateTime() 176 | return self.playback_elapsed 177 | end 178 | 179 | --- Returns the current playback state of the stopwatch. 0 for STOPWATCH_STOPPED, 1 for STOPWATCH_PLAYING, 2 for STOPWATCH_PAUSED 180 | ---@return number state Playback state 181 | function Stopwatch:GetState() 182 | return self.state 183 | end 184 | 185 | --- Sets the stopwatch to loop. Won't call the callback if it is looping. 186 | ---@param loop boolean Whether it's looping 187 | ---@return Stopwatch self 188 | function Stopwatch:SetLooping(loop) 189 | if self.looping ~= loop then 190 | self.looping = loop 191 | timer.Adjust( self.timerid, self.delay, 0, nil ) 192 | end 193 | return self 194 | end 195 | 196 | --- Returns if the stopwatch is looping 197 | ---@return boolean looping 198 | function Stopwatch:GetLooping() 199 | return self.looping 200 | end 201 | 202 | _G.StopWatch = Stopwatch 203 | _G.Stopwatch = Stopwatch 204 | 205 | return Stopwatch -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebAudio [![Release Shield](https://img.shields.io/github/v/release/thevurv/WebAudio)](https://github.com/thevurv/WebAudio/releases/latest) [![License](https://img.shields.io/github/license/thevurv/WebAudio?color=red)](https://opensource.org/licenses/MIT) [![Linter Badge](https://github.com/thevurv/WebAudio/workflows/Linter/badge.svg)](https://github.com/thevurv/WebAudio/actions) [![github/thevurv](https://img.shields.io/discord/824727565948157963?label=Discord&logo=discord&logoColor=ffffff&labelColor=7289DA&color=2c2f33)](https://discord.gg/yXKMt2XUXm) [![Workshop Subscribers](https://img.shields.io/steam/subscriptions/2466875474?color=yellow&logo=steam)](https://steamcommunity.com/sharedfiles/filedetails/?id=2466875474) 2 | 3 | 4 | This is an addon for garrysmod that adds a serverside ``WebAudio`` class which can be used to interact with clientside IGmodAudioChannels asynchronously. 5 | It also adds an extension for Expression2 to interact with these in a safe manner. 6 | 7 | You can consider this as a safe and more robust replacement for the popular Streamcore addon, and I advise you to replace it for the safety of your server and its population. 8 | ``Documentation`` can be found at the bottom of this file 9 | 10 | To convert your Streamcore contraptions to *WebAudio*, see [from Streamcore to WebAudio](https://github.com/thevurv/WebAudio/wiki/From-StreamCore-To-WebAudio) 11 | 12 | ## Features 13 | * Client and Serverside Whitelists, both are customizable with simple patterns or more advanced lua patterns. 14 | * The ``WebAudio`` class and api for that allows for interfacing with IGModAudioChannels from the server, changing the volume, position and time of the audio streams dynamically. 15 | * Comes with an Expression2 type that binds to the WebAudio lua class. 16 | 17 | ## Why use this over streamcore 18 | If you do not know already, Streamcore is a dangerous addon in several ways. 19 | * Allows for ANYONE to download ANYTHING to everyone on the server's computers. 20 | * Allows for people to crash / lag everyone easily. 21 | * ~~Doesn't allow you to disable it.~~ (There is apparently a way to disable streamcore through ``streamcore_disabled``. It was added ~4 years after the addon was created only after people kept bugging the creator that maybe it isn't a good idea to allow people to download ANYTHING to your computer with no shut off switch.) 22 | 23 | WebAudio aims to easily solve these issues by: 24 | * Having proper net limits 25 | * Using a customizable whitelist for both your server & its clients. 26 | * Containing a flexible but safe default whitelist that only allows for large or trusted domains as to not allow for malicious requests. 27 | 28 | ## Contributing 29 | See CONTRIBUTING.md 30 | 31 | ## Convars 32 | | Realm | Name | Default Value | Description | 33 | |--------|---------------|---------------|----------------------------------------------------------------------------------------------------------------------------| 34 | | SHARED | wa_enable | 1 | Whether WebAudio is enabled for you or the whole server. If you set this to 0, it will purge all currently running streams | 35 | | SERVER | wa_admin_only | 0 | Restrict E2 WebAudio access to admins. 0 is everyone, 1 is >=admin, 2 is super admins only. | 36 | | SERVER | wa_stream_max | 5 | Max number of E2 WebAudio streams a player can have | 37 | | SHARED | wa_volume_max | 300 | Max volume of streams, will clamp the volume of streams to this on both the client and on the server | 38 | | SHARED | wa_radius_max | 3000 | Max distance where WebAudio streams can be heard from their origin. | 39 | | SHARED | wa_fft_enable | 1 | Whether FFT data is enabled for the server / your client. You shouldn't need to disable it as it is very lightweight | 40 | | CLIENT | wa_verbosity | 1 | Verbosity of console notifications. 2 => URL/Logging + Extra Info, 1 => Only warnings/errors, 0 => Nothing (Avoid this) | 41 | | SERVER | wa_sc_compat | 0 | Whether streamcore-compatible functions should be generated for E2 (for backwards compat) | 42 | 43 | ## Concommands 44 | | Realm | Name | Description | 45 | |--------|---------------------|----------------------------------------------------------------------------------------------------------------| 46 | | SHARED | wa_purge | Purges all currently running WebAudio streams and does not receive any further net updates with their objects. | 47 | | SHARED | wa_reload_whitelist | Refreshes your whitelist located at data/webaudio_whitelist.txt | 48 | | SHARED | wa_list | Prints a list of currently playing WebAudio streams (As long as their owner IsValid) with their url, id & owner| 49 | | SHARED | wa_help | Prints the link to the github to your console | 50 | | CLIENT | wa_display | Displays active WebAudio streams to your screen w/ steamid and stream id | 51 | 52 | ## Functions 53 | 54 | ``webaudio webAudio(string url)`` 55 | Returns a **WebAudio** object of that URL as long as it is whitelisted by the server. 56 | Has a burst cooldown between calls. If you can't create a webAudio object, it'll error, so check ``webAudioCanCreate`` before calling this! You can create all of your streams in one tick but will have to wait 300ms for each slot to regenerate. 57 | 58 | ``number webAudiosLeft()`` 59 | Returns how many WebAudios you can create. 60 | 61 | ``number webAudioCanCreate()`` 62 | Returns whether you can create a webaudio stream. Checks cooldown and whether you have a slot for another stream left. 63 | 64 | ``number webAudioCanCreate(string url)`` 65 | Same as ``webAudioCanCreate()``, but also checks if the url given is whitelisted so you don't error on webAudio calls. 66 | 67 | ``number webAudioEnabled()`` 68 | Returns whether webAudio is enabled for use on the server. 69 | 70 | ``number webAudioAdminOnly()`` 71 | Returns 0 if webAudio is available to everyone, 1 if only admins, 2 if only superadmins. 72 | 73 | ### WebAudio Type Methods 74 | 75 | ## Setters 76 | 77 | ``void webaudio:setDirection(vector dir)`` 78 | Sets the direction in which the WebAudio stream is playing towards. Does not update the object. 79 | 80 | ``void webaudio:setPos(vector pos)`` 81 | Sets the 3D Position of the WebAudio stream without updating it. Does not update the object. 82 | 83 | ``void webaudio:setVolume(number volume)`` 84 | Sets the volume of the WebAudio stream, in percent format. 200 is 2x as loud as normal, 100 is default. Will error if it is past ``wa_volume_max``. 85 | 86 | ``void webaudio:setTime(number time)`` 87 | Sets the time in which the WebAudio stream should seek to in playing the URL. Does not update the object. 88 | 89 | ``void webaudio:setPlaybackRate(number rate)`` 90 | Sets the playback speed of a webaudio stream. 2 is twice as fast, 0.5 being half, etc. Does not update the object. 91 | 92 | ``void webaudio:setParent(entity e)`` 93 | Parents the stream position to e, local to the entity. If you've never set the position before, will be parented to the center of the prop. Returns 1 if successfully parented or 0 if prop wasn't valid. Does not update the object. 94 | 95 | ``void webaudio:setParent()`` 96 | Unparents the stream from the currently parented entity. Does not update the object. 97 | 98 | ``void webaudio:parentTo(entity e)`` 99 | Alias of webaudio:setParent(e) Parents the stream position to e, local to the entity. If you've never set the position before, will be parented to the center of the prop. Does not update the object. 100 | 101 | ``void webaudio:unparent()`` 102 | Alias of webaudio:setParent(), Unparents the stream from the currently parented entity. Does not update the object. 103 | 104 | ``void webaudio:setRadius(number radius)`` 105 | Sets the radius in which to the stream will be heard in. Default is 200 and (default) max is 1500. Does not update the object. 106 | 107 | ``void webaudio:setLooping(number looping)`` 108 | Sets the stream to loop if n ~= 0, else stops looping. 109 | 110 | ``void webaudio:set3DEnabled(number enabled)`` 111 | By default WebAudio streams are 3D, so if enabled is 0 it will be set to "2D", 112 | so the sound will be on the player's position if they are within the radius of the stream. 113 | 114 | ## Special 115 | 116 | ``number webaudio:pause()`` 117 | Pauses the stream where it is currently playing, Returns 1 or 0 if could successfully do so, because of quota. Automatically calls updates self internally. 118 | 119 | ``number webaudio:play()`` 120 | Starts the stream or continues where it left off after pausing, Returns 1 or 0 if could successfully do so from quota. Automatically updates self internally. 121 | 122 | ``void webaudio:destroy()`` 123 | Destroys the WebAudio object, rendering it useless. It will silently fail when trying to use it from here on! This gives you another slot to make a WebAudio object 124 | 125 | ``number webaudio:update()`` 126 | Sends all of the information of the object given by functions like setPos and setTime to the client. You need to call this after running functions without running ``:play()`` or ``:pause()`` on them since those sync with the client. Returns 1 if could update, 0 if hit transmission quota 127 | 128 | ## Getters 129 | 130 | ``number webaudio:isValid()`` 131 | Returns 1 or 0 for whether the stream is valid and not destroyed. 132 | 133 | ``number webaudio:isParented()`` 134 | Returns 1 or 0 for whether the stream is parented 135 | 136 | ``vector webaudio:getPos()`` 137 | Returns the position of the stream set by setPos. Doesn't work with parented streams since they do that math on the client. 138 | 139 | ``vector webaudio:getVolume()`` 140 | Returns the volume of the stream set by setVolume 141 | 142 | ``number webaudio:getRadius()`` 143 | Returns the radius of the stream set by setRadius 144 | 145 | ``number webaudio:getTime()`` 146 | Returns the current playback time of the stream in seconds. 147 | 148 | ``number webaudio:getState()`` 149 | Returns the state of the stream, 0 = STOPPED, 1 = PLAYING, 2 = PAUSED, Use the _CHANNEL* constants to get these values easily. (_CHANNEL_STOPPED is 0 for example) 150 | 151 | ``number webaudio:getLength()`` 152 | Returns the playback duration / length of the stream. Returns -1 if we haven't received the length yet. 153 | 154 | ``string webaudio:getFileName()`` 155 | Returns the file name of the webaudio stream. Usually but not always returns the URL of the stream, and will return "" if we haven't received the filename yet. 156 | 157 | ``number webaudio:getLooping()`` 158 | Returns if the stream is looping, set by setLooping 159 | 160 | ``number webaudio:get3DEnabled()`` 161 | Returns if the webaudio is in 3D mode. This is true by default. 162 | 163 | ``array webaudio:getFFT()`` 164 | Returns an array of 64 FFT values from 0-255. 165 | 166 | ## Ignore System 167 | > This is a system similar to how you'd use holoVisible to hide holograms from others. 168 | 169 | ``array webaudio:setIgnored(entity ply, number ignored)`` 170 | If ignored is not 0, blocks the given user from hearing the stream. Else, unblocks the user. 171 | 172 | ``array webaudio:setIgnored(array plys, number ignored)`` 173 | If ignored is not 0, sets the stream to ignore certain players. Else, unblocks them all. 174 | 175 | ``number webaudio:getIgnored(entity ply)`` 176 | Returns if the user is blocked from hearing the stream. Note this also counts if they purged the webaudio stream themself, but doesn't count if they have webaudio disabled. Set by setIgnored 177 | -------------------------------------------------------------------------------- /lua/webaudio/receiver.lua: -------------------------------------------------------------------------------- 1 | -- This file assumes ``autorun/webaudio.lua`` ran right before it. 2 | 3 | local Common = WebAudio.Common 4 | local wa_warn, wa_notify = Common.warn, Common.notify 5 | 6 | local math_min = math.min 7 | 8 | -- Convars 9 | local Enabled, FFTEnabled = Common.WAEnabled, Common.WAFFTEnabled 10 | local MaxVolume, MaxRadius = Common.WAMaxVolume, Common.WAMaxRadius 11 | local Verbosity = Common.WAVerbosity 12 | 13 | local AwaitingChanges = {} 14 | local updateObject -- To be declared below 15 | 16 | net.Receive("wa_fft", function(len) 17 | local stream = WebAudio.readStream() 18 | if not stream then return end 19 | if not FFTEnabled:GetBool() then return end 20 | 21 | local bass = stream.bass 22 | if not bass then return end 23 | if bass:GetState() ~= GMOD_CHANNEL_PLAYING then return end 24 | 25 | local t = {} 26 | local filled = math_min( bass:FFT(t, FFT_256), 128 ) 27 | local samp_len = WebAudio.FFTSAMP_LEN 28 | 29 | net.Start("wa_fft", true) 30 | WebAudio.writeID(stream.id) 31 | for k = 2, filled, 2 do 32 | -- Multiply the small decimal to a number we will floor and write as a UInt. 33 | -- If the number is smaller, it will be more precise for higher numbers. 34 | -- If it's larger, it will be more precise for smaller number fft magnitudes. 35 | local v = math_min(t[k] * 1e4, 255) 36 | if v < 1 then break end -- Assume the rest of the data is 0. 37 | net.WriteUInt(v, samp_len) 38 | end 39 | net.SendToServer() 40 | end) 41 | 42 | timer.Create("wa_think", 100 / 1000, 0, function() 43 | local LocalPlayer = LocalPlayer() 44 | if not IsValid(LocalPlayer) then return end 45 | 46 | local player_pos = LocalPlayer:GetPos() 47 | for id, stream in pairs( WebAudio.getList() ) do 48 | local bass = stream.bass 49 | 50 | if bass then 51 | -- Handle Parenting 52 | local parent, parent_pos = stream.parent, stream.parent_pos 53 | if IsValid(parent) then 54 | if parent_pos then 55 | -- Offset from prop 56 | local position = parent:LocalToWorld(parent_pos) 57 | if stream.pos ~= position then 58 | bass:SetPos( position ) 59 | stream.pos = position 60 | end 61 | else 62 | -- Origin of prop 63 | local position = parent:GetPos() + parent:GetVelocity() * 0.05 64 | if stream.pos ~= position then 65 | bass:SetPos( position ) 66 | stream.pos = position 67 | end 68 | end 69 | end 70 | 71 | -- Manually handle volume as you go farther from the stream. 72 | if stream.pos then 73 | local dist_to_stream = player_pos:DistToSqr( stream.pos ) 74 | 75 | -- Could also ( 1 - math_min(dist_to_stream / stream.radius, 1) ) 76 | if dist_to_stream > stream.radius_sqr then 77 | -- Stream is too far away. 78 | bass:SetVolume( 0 ) 79 | elseif stream.mode == WebAudio.MODE_2D then 80 | bass:SetVolume( stream.volume ) 81 | elseif dist_to_stream > stream.radius_sqr * 0.5 then 82 | -- Stream is kinda far, now start smoothing 83 | bass:SetVolume( stream.volume - ( dist_to_stream / stream.radius_sqr ) * stream.volume * 1.5 + stream.volume / 2 ) 84 | else 85 | -- Stream is pretty close. Slow smooth 86 | bass:SetVolume( stream.volume - ( dist_to_stream / stream.radius_sqr ) * stream.volume / 2 ) 87 | end 88 | end 89 | end 90 | end 91 | end) 92 | 93 | ---@param url string 94 | ---@param onSuccess fun() 95 | ---@param onError fun(err: string) 96 | local function checkStreamContents(url, onSuccess, onError) 97 | if hook.Run("WA_ShouldCheckStreamContent", url) == false then 98 | return onSuccess() 99 | end 100 | 101 | http.Fetch(url, function(body, _, _) 102 | if body:find("#EXTM3U", 1, true) then 103 | return onError("Cannot create stream with unwhitelisted file format (m3u)") 104 | end 105 | 106 | if body:find("[playlist]", 1, true) then 107 | return onError("Cannot create stream with unwhitelisted file format (pls)") 108 | end 109 | 110 | onSuccess() 111 | end, function(err) 112 | onError("HTTP error:" .. err) 113 | end) 114 | end 115 | 116 | net.Receive("wa_create", function(len) 117 | local id, url, owner = WebAudio.readID(), net.ReadString(), net.ReadEntity() 118 | local verbosity = Verbosity:GetInt() 119 | 120 | if not IsValid(owner) then return end -- Owner left ? 121 | 122 | local warn, notify 123 | if verbosity >= 2 then 124 | -- Very verbose (old default) 125 | warn = wa_warn 126 | notify = wa_notify 127 | elseif verbosity >= 1 then 128 | -- Warnings only (new default) 129 | warn = wa_warn 130 | function notify(...) end 131 | else 132 | function warn(...) end 133 | function notify(...) end 134 | end 135 | 136 | if not Enabled:GetBool() then 137 | -- Shouldn't happen anymore as net messages won't send to users who have webaudio disabled. 138 | return notify( 139 | "%s(%s) attempted to create a WebAudio object with url [%q], but you have WebAudio disabled!", 140 | owner:Nick(), 141 | owner:SteamID64() or "multirun", 142 | url 143 | ) 144 | end 145 | 146 | if not WebAudio.isWhitelistedURL(url) then 147 | return warn("User %s(%s) tried to create unwhitelisted WebAudio object with url [%q]", owner:Nick(), owner:SteamID64() or "multirun", url) 148 | end 149 | 150 | local is_stream_owner = owner == LocalPlayer() 151 | 152 | -- If a stream failed to the point where it wouldn't be able to function for anyone, 153 | -- Send feedback to the server to destroy the object. 154 | local function streamFailed() end 155 | 156 | if is_stream_owner then 157 | function streamFailed() 158 | net.Start("wa_info", true) 159 | WebAudio.writeID(id) 160 | net.WriteBool(true) 161 | net.SendToServer() 162 | end 163 | end 164 | 165 | notify("User %s(%s) created WebAudio object with url [%q]", owner:Nick(), owner:SteamID64() or "multirun", url) 166 | 167 | checkStreamContents(url, function() 168 | sound.PlayURL(url, "3d noblock noplay", function(bass, errid, errname) 169 | if errid then 170 | streamFailed() 171 | return warn("Error when creating WebAudio receiver with id %d, Error [%s]", id, errname) 172 | end 173 | 174 | if not bass:IsValid() then 175 | streamFailed() 176 | return warn("WebAudio object with id [%d]'s IGModAudioChannel object was null!", id) 177 | end 178 | 179 | local self = WebAudio.getFromID(id) 180 | if not ( self and self:IsValid() ) then 181 | bass:Stop() 182 | bass = nil 183 | streamFailed() 184 | 185 | if is_stream_owner then 186 | -- Have it only warn for the owner in preparation for #33 187 | -- Shouldn't be possible right now unless some one sends a destroy net message to a random id with sv lua. 188 | warn("Invalid WebAudio with id [" .. id .. "], did you destroy it before it even loaded?") 189 | end 190 | return 191 | end 192 | 193 | if bass:IsBlockStreamed() then 194 | streamFailed() 195 | bass:Stop() 196 | bass = nil 197 | return warn("URL [%s] was incompatible for WebAudio; It is block-streamed!", url) 198 | end 199 | 200 | self.bass = bass 201 | self.length = bass:GetLength() 202 | self.filename = bass:GetFileName() 203 | 204 | local changes_awaiting = AwaitingChanges[id] 205 | if changes_awaiting then 206 | updateObject(id, changes_awaiting, true, false) 207 | AwaitingChanges[id] = nil 208 | end 209 | 210 | if owner == LocalPlayer() then 211 | if not self:IsValid() then 212 | -- It was destroyed inside of AwaitingChanges. Usually some dude spamming it. 213 | return 214 | end 215 | -- Only send WebAudio info if LocalPlayer is the owner of the WebAudio object. Will also check on server to avoid abuse. 216 | net.Start("wa_info", true) 217 | WebAudio.writeID(id) 218 | net.WriteBool(false) 219 | local continuous = self.length < 0 220 | net.WriteBool(continuous) -- If the stream is continuous, it should return something less than 0. 221 | if not continuous then 222 | net.WriteUInt(self.length, 16) 223 | end 224 | net.WriteString(self.filename) 225 | net.SendToServer() 226 | end 227 | end) 228 | end, function(err) 229 | streamFailed() 230 | return warn("Error when creating WebAudio object with id: %s, User %s(%s), Error: %s", id, owner:Nick(), owner:SteamID64() or "multirun", err) 231 | end) 232 | 233 | WebAudio.new(url, owner, nil, id) -- Register object 234 | end) 235 | 236 | local Modify = Common.Modify 237 | local hasModifyFlag = Common.hasModifyFlag 238 | 239 | --- Stores changes on the Receiver object 240 | --- @param id number ID of the Receiver Object, to be used to search the table of 'WebAudio.getList()' 241 | --- @param modify_enum number Mixed bitwise flag that will be sent by the server to determine what changed in an object to avoid wasting a lot of bits for every piece of information. 242 | --- @param handle_bass boolean Whether this object has a 'bass' object. If so, we can just immediately apply the changes to the object. 243 | --- @param inside_net boolean Whether this function is inside the net message that contains the new information. If not, we're most likely just applying object changes to the receiver after waiting for the IGmodAudioChannel object to be created. 244 | function updateObject(id, modify_enum, handle_bass, inside_net) 245 | -- Object destroyed 246 | local self = WebAudio.getFromID(id) 247 | local bass = self.bass 248 | 249 | -- Keep in mind that the order of these needs to sync with wa_interface's reading order. 250 | if hasModifyFlag(modify_enum, Modify.destroyed) then 251 | -- Don't destroy until we have the bass object. 252 | if handle_bass then 253 | self:Destroy(false) 254 | self = nil 255 | end 256 | return 257 | end 258 | 259 | -- Volume changed 260 | if hasModifyFlag(modify_enum, Modify.volume) then 261 | if inside_net then self.volume = math_min(net.ReadFloat(), MaxVolume:GetInt() / 100) end 262 | if handle_bass then 263 | bass:SetVolume(self.volume) 264 | end 265 | end 266 | 267 | -- Playback time changed 268 | if hasModifyFlag(modify_enum, Modify.time) then 269 | if inside_net then self.time = net.ReadUInt(16) end 270 | if handle_bass then 271 | bass:SetTime(self.time) -- 18 hours max, if you need more, wtf.. 272 | end 273 | end 274 | 275 | -- 3D Position changed 276 | if hasModifyFlag(modify_enum, Modify.pos) then 277 | if inside_net then self.pos = net.ReadVector() end 278 | if handle_bass then 279 | bass:SetPos(self.pos, nil) 280 | end 281 | end 282 | 283 | -- Direction changed. 284 | if hasModifyFlag(modify_enum, Modify.direction) then 285 | if inside_net then self.direction = net.ReadVector() end 286 | if handle_bass then 287 | bass:SetPos(self.pos, self.direction) 288 | end 289 | end 290 | 291 | -- Playback rate changed 292 | if hasModifyFlag(modify_enum, Modify.playback_rate) then 293 | if inside_net then self.playback_rate = net.ReadFloat() end 294 | if handle_bass then 295 | bass:SetPlaybackRate(self.playback_rate) 296 | end 297 | end 298 | 299 | -- Radius changed 300 | if hasModifyFlag(modify_enum, Modify.radius) then 301 | if inside_net then 302 | self.radius = math_min(net.ReadUInt(16), MaxRadius:GetInt()) 303 | self.radius_sqr = self.radius * self.radius 304 | end 305 | 306 | if handle_bass and self.pos then 307 | local dist_to_stream = LocalPlayer():GetPos():Distance( self.pos ) 308 | bass:SetVolume( self.volume * ( 1 - math_min(dist_to_stream / self.radius, 1) ) ) 309 | end 310 | end 311 | 312 | if hasModifyFlag(modify_enum, Modify.looping) then 313 | if inside_net then self.looping = net.ReadBool() end 314 | if handle_bass then 315 | bass:EnableLooping(self.looping) 316 | end 317 | end 318 | 319 | if hasModifyFlag(modify_enum, Modify.mode) then 320 | if inside_net then self.mode = net.ReadBit() end 321 | if handle_bass then 322 | bass:Set3DEnabled(self.mode == WebAudio.MODE_3D) 323 | end 324 | end 325 | 326 | -- Was parented or unparented 327 | if hasModifyFlag(modify_enum, Modify.parented) then 328 | if inside_net then 329 | self.parented = net.ReadBool() 330 | if self.parented then 331 | local ent = net.ReadEntity() 332 | if not IsValid(ent) then 333 | -- incredibly rare edge case that was never even reproduced, but w/e 334 | return 335 | end 336 | 337 | self.parent = ent 338 | if self.pos ~= nil then 339 | -- We've initialized and received a changed position before 340 | self.parent_pos = ent:WorldToLocal(self.pos) 341 | else 342 | -- self.pos hasn't been touched, play the sound at the entity's position. 343 | self.parent_pos = nil 344 | end 345 | else 346 | self.parent = nil 347 | end 348 | end 349 | 350 | if handle_bass and self.parented then 351 | ---@type GEntity (Absolutely sure it is parented) 352 | local parent = self.parent 353 | if IsValid(parent) then 354 | local parent_pos = self.parent_pos 355 | local position = parent_pos and parent:LocalToWorld(parent_pos) or parent:GetPos() 356 | bass:SetPos(position, nil) 357 | self.pos = position 358 | end 359 | end 360 | end 361 | 362 | -- Playing / Paused state changed. Should always be at the bottom here 363 | if hasModifyFlag(modify_enum, Modify.playing) then 364 | if inside_net then self.playing = net.ReadBool() end 365 | -- Should always be true so.. 366 | if handle_bass then 367 | if self.playing then 368 | -- If changed to be playing, play. Else pause 369 | bass:Play() 370 | else 371 | bass:Pause() 372 | end 373 | end 374 | end 375 | end 376 | 377 | net.Receive("wa_change", function(len) 378 | local id, modify_enum = WebAudio.readID(), WebAudio.readModify() 379 | local obj = WebAudio.getFromID(id) 380 | 381 | if AwaitingChanges[id] == modify_enum then return end 382 | if not obj then return end -- Object was destroyed on the client for whatever reason. Most likely reason is wa_purge 383 | 384 | if obj.bass then 385 | -- Store and handle changes 386 | updateObject(id, modify_enum, true, true) 387 | else 388 | -- We don't have the bass object yet. Store the changes to update the object with later. 389 | AwaitingChanges[id] = modify_enum 390 | updateObject(id, modify_enum, false, true) 391 | end 392 | end) 393 | 394 | --- Stops every currently existing stream 395 | --- @param write_ids boolean Whether to net write IDs or not while looping through the streams. Used for wa_ignore. 396 | local function stopStreams(write_ids) 397 | for id, stream in WebAudio.getIterator() do 398 | if stream then 399 | stream:Destroy() 400 | if write_ids then 401 | -- If we are in wa_purge 402 | WebAudio.writeID(id) 403 | end 404 | end 405 | end 406 | end 407 | 408 | concommand.Add("wa_purge", function() 409 | local stream_count = WebAudio.getCountActive() 410 | if stream_count == 0 then return end 411 | 412 | net.Start("wa_ignore", true) 413 | net.WriteUInt(stream_count, 8) 414 | stopStreams(true) 415 | net.SendToServer() 416 | end, nil, "Purges all of the currently playing WebAudio streams", 0) 417 | 418 | cvars.RemoveChangeCallback("wa_enable", "wa_enable") 419 | 420 | --- When the client toggles wa_enable send this info to the server to stop sending net messages to them. 421 | cvars.AddChangeCallback("wa_enable", function(convar, old, new) 422 | local enabled = new ~= "0" 423 | if not enabled then 424 | stopStreams(false) 425 | end 426 | net.Start("wa_enable", true) 427 | net.WriteBool(enabled) -- Tell the server to subscribe/unsubscribe us from net messages 428 | net.SendToServer() 429 | end, "wa_enable") 430 | -------------------------------------------------------------------------------- /lua/webaudio/interface.lua: -------------------------------------------------------------------------------- 1 | -- This file assumes ``autorun/webaudio.lua`` ran right before it. 2 | 3 | local Common = _G.WebAudio.Common 4 | local Modify = Common.Modify 5 | 6 | --- Object networking 7 | util.AddNetworkString("wa_create") -- To send to the client to create a Clientside WebAudio struct 8 | util.AddNetworkString("wa_change") -- To send to the client to modify Client WebAudio structs 9 | util.AddNetworkString("wa_ignore") -- To receive from the client to make sure to ignore players to send to in WebAudio transmissions 10 | util.AddNetworkString("wa_enable") -- To receive from the client to make sure people with wa_enable 0 don't get WebAudio transmissions 11 | util.AddNetworkString("wa_info") -- To receive information about BASS / IGmodAudioChannel streams from clients that create them. 12 | util.AddNetworkString("wa_fft") -- Receive fft data. 13 | 14 | ---@class WebAudio 15 | local WebAudio = Common.WebAudio 16 | 17 | --- TODO: Make this nicer? 18 | -- I would use a CRecipientFilter but I also need to add the people with wa_enable to 0 rather than just the people who used wa_purge to kill certain objects. 19 | -- If you could merge two CRecipientFilters that'd be cool. 20 | local StreamDisabledPlayers = { -- People who have wa_enabled set to 0 21 | __hash = {}, -- If __hash[ply] is false, the player is already subscribed. We use this to not add multiple players and get the subbed status 22 | __net = {} -- Net friendly array 23 | } 24 | 25 | --- Adds a modify flag to the payload to be sent on transmission 26 | --- @param n number Modify flag from the Modify struct in wa_common 27 | --- @return boolean? # Whether it modified. Will return nil if stream is destroyed. 28 | function WebAudio:AddModify(n) 29 | if self:IsDestroyed() then return end 30 | self.modified = bit.bor(self.modified, n) 31 | end 32 | 33 | --#region Setters 34 | 35 | --- Sets the volume of the stream 36 | -- Does not transmit 37 | --- @param vol number Float Volume (1 is 100%, 2 is 200% ..) 38 | --- @return boolean? # Successfully set time, will return nil if stream or 'vol' are invalid or 'vol' didn't change. 39 | function WebAudio:SetVolume(vol) 40 | if self:IsDestroyed() then return end 41 | if isnumber(vol) and self.volume ~= vol then 42 | self.volume = vol 43 | self:AddModify(Modify.volume) 44 | return true 45 | end 46 | end 47 | 48 | --- Sets the current playback time of the stream. 49 | -- Does not transmit 50 | --- @param time number UInt16 Playback time 51 | --- @return boolean? # Successfully set time, will return nil if stream is invalid. 52 | function WebAudio:SetTime(time) 53 | if self:IsDestroyed() then return end 54 | if isnumber(time) then 55 | self.time = time 56 | self.stopwatch:SetTime(time) 57 | self:AddModify(Modify.time) 58 | return true 59 | end 60 | end 61 | 62 | --- Sets the position of the stream. 63 | --- @param pos GVector Position 64 | --- @return boolean # Successfully set position, will return nil if stream or 'pos' are invalid or if 'pos' didn't change. 65 | function WebAudio:SetPos(pos) 66 | if self:IsDestroyed() then return end 67 | if self:IsParented() then return end 68 | if isvector(pos) and self.pos ~= pos then 69 | self.pos = pos 70 | self:AddModify(Modify.pos) 71 | return true 72 | end 73 | end 74 | 75 | --- Sets the direction in which the stream will play 76 | --- @param dir GVector Direction to set to 77 | --- @return boolean? # Successfully set direction, will return nil if stream or 'dir' are invalid or if 'dir' didn't change. 78 | function WebAudio:SetDirection(dir) 79 | if self:IsDestroyed() then return end 80 | if isvector(dir) and self.direction ~= dir then 81 | self.direction = dir 82 | self:AddModify(Modify.direction) 83 | return true 84 | end 85 | end 86 | 87 | --- Sets the playback rate of the stream. 88 | --- @param rate number Playback rate. Float64 that clamps to 255. 89 | --- @return boolean? # Successfully set rate, will return nil if stream or 'rate' are invalid or if 'rate' didn't change. 90 | function WebAudio:SetPlaybackRate(rate) 91 | if self:IsDestroyed() then return end 92 | if not isnumber(rate) then return end 93 | 94 | rate = math.min(rate, 255) 95 | if self.playback_rate ~= rate then 96 | self.stopwatch:SetRate(rate) 97 | 98 | self.playback_rate = rate 99 | self:AddModify(Modify.playback_rate) 100 | return true 101 | end 102 | end 103 | 104 | --- Sets the radius of the stream. Uses Set3DFadeDistance internally. 105 | --- @param radius number UInt16 radius 106 | --- @return boolean? # Successfully set radius, will return nil if the stream or 'radius' are invalid or if radius didn't change. 107 | function WebAudio:SetRadius(radius) 108 | if self:IsDestroyed() then return end 109 | if isnumber(radius) and self.radius ~= radius then 110 | self.radius = radius 111 | self.radius_sqr = radius * radius 112 | 113 | self:AddModify(Modify.radius) 114 | return true 115 | end 116 | end 117 | 118 | --- Sets the parent of the WebAudio object. Nil to unparent 119 | --- @param ent GEntity? Entity to parent to or nil to unparent. 120 | --- @return boolean? # Whether it was successfully parented. Returns nil if the stream is invalid. 121 | function WebAudio:SetParent(ent) 122 | if self:IsDestroyed() then return end 123 | if IsEntity(ent) and IsValid(ent) then 124 | self.parented = true 125 | self.parent = ent 126 | else 127 | self.parented = false 128 | self.parent = nil 129 | end 130 | self:AddModify(Modify.parented) 131 | return true 132 | end 133 | 134 | --- Makes the stream loop or stop looping. 135 | --- @param loop boolean Whether it should be looping 136 | --- @return boolean? # If we set the value or not. Returns nil if the stream isn't valid or if the value didn't change. 137 | function WebAudio:SetLooping(loop) 138 | if self:IsDestroyed() then return end 139 | if self.looping ~= loop then 140 | self.stopwatch:SetLooping(loop) 141 | self.looping = loop 142 | self:AddModify(Modify.looping) 143 | return true 144 | end 145 | end 146 | 147 | --- Enables/Disables 3D Mode for a stream. 148 | ---@param is3d boolean 149 | ---@return boolean? # If we set the value or not. Returns nil if the stream isn't valid or if the value didn't change. 150 | function WebAudio:Set3DEnabled(is3d) 151 | if self:IsDestroyed() then return end 152 | local desired = is3d and WebAudio.MODE_3D or WebAudio.MODE_2D 153 | 154 | if self.mode ~= desired then 155 | self.mode = desired 156 | self:AddModify(Modify.mode) 157 | return true 158 | end 159 | end 160 | 161 | --#endregion 162 | 163 | --#region Special 164 | 165 | --- Resumes or starts the stream. 166 | --- @return boolean? # Successfully played, will return nil if the stream is destroyed or if already playing 167 | function WebAudio:Play() 168 | if self:IsDestroyed() then return end 169 | 170 | if self.playing == false then 171 | self:AddModify(Modify.playing) 172 | 173 | if self.stopwatch.state == STOPWATCH_STOPPED then 174 | self.stopwatch:Start() 175 | else 176 | self.stopwatch:Play() 177 | end 178 | 179 | self.playing = true 180 | self:Transmit(false) 181 | return true 182 | end 183 | end 184 | 185 | --- Pauses the stream and automatically transmits. 186 | --- @return boolean? # Successfully paused, will return nil if the stream is destroyed or if already paused 187 | function WebAudio:Pause() 188 | if self:IsDestroyed() then return end 189 | 190 | if self.playing then 191 | self:AddModify(Modify.playing) 192 | self.stopwatch:Pause() 193 | self.playing = false 194 | self:Transmit(false) 195 | 196 | return true 197 | end 198 | end 199 | 200 | --#endregion 201 | 202 | --#region Getters 203 | 204 | local LastUpdates = setmetatable({}, { 205 | __mode = "k" 206 | }) 207 | 208 | --- Returns the fast fourier transform of the webaudio stream. 209 | -- Returns nil if invalid. 210 | --- @param update boolean Whether to update the fft values. 211 | --- @return table # FFT 212 | function WebAudio:GetFFT(update, cooldown) 213 | if self:IsDestroyed() then return end 214 | if update and IsValid(self.owner) then 215 | if cooldown then 216 | local now = SysTime() 217 | local last = LastUpdates[self] or 0 218 | if now - last > cooldown then 219 | LastUpdates[self] = now 220 | else 221 | return self.fft 222 | end 223 | end 224 | net.Start("wa_fft", true) 225 | WebAudio.writeID( self.id ) 226 | net.Send(self.owner) 227 | end 228 | return self.fft 229 | end 230 | 231 | --#endregion 232 | 233 | local hasModifyFlag = Common.hasModifyFlag 234 | 235 | --- Transmits all stored data on the server about the WebAudio object to the clients 236 | ---@param force_ignored boolean # Whether to also transmit to those who are ignored by it 237 | --- @return boolean # If successfully transmitted. 238 | function WebAudio:Transmit(force_ignored) 239 | if self:IsDestroyed() then return end 240 | 241 | net.Start("wa_change", not force_ignored) 242 | -- Always present values 243 | WebAudio.writeID(self.id) 244 | local modified = self.modified 245 | WebAudio.writeModify(modified) 246 | 247 | if not hasModifyFlag(modified, Modify.destroyed) then 248 | if hasModifyFlag(modified, Modify.volume) then 249 | net.WriteFloat(self.volume) 250 | end 251 | 252 | if hasModifyFlag(modified, Modify.time) then 253 | net.WriteUInt(self.time, 16) 254 | end 255 | 256 | if hasModifyFlag(modified, Modify.pos) then 257 | net.WriteVector(self.pos) 258 | end 259 | 260 | if hasModifyFlag(modified, Modify.direction) then 261 | net.WriteVector(self.direction) 262 | end 263 | 264 | if hasModifyFlag(modified, Modify.playback_rate) then 265 | net.WriteFloat(self.playback_rate) 266 | end 267 | 268 | if hasModifyFlag(modified, Modify.radius) then 269 | net.WriteUInt(self.radius, 16) 270 | end 271 | 272 | if hasModifyFlag(modified, Modify.looping) then 273 | net.WriteBool(self.looping) 274 | end 275 | 276 | if hasModifyFlag(modified, Modify.mode) then 277 | net.WriteBit(self.mode == 1) 278 | end 279 | 280 | if hasModifyFlag(modified, Modify.parented) then 281 | net.WriteBool(self.parented) 282 | if self.parented then 283 | net.WriteEntity(self.parent) 284 | end 285 | end 286 | 287 | if hasModifyFlag(modified, Modify.playing) then 288 | net.WriteBool(self.playing) 289 | end 290 | end 291 | self:Broadcast(force_ignored) 292 | self.modified = 0 -- Reset any modifications 293 | return true 294 | end 295 | 296 | --#region Subscriptions 297 | 298 | --- Registers an object to not send any net messages to from WebAudio transmissions. 299 | -- This is called by clientside destruction. 300 | --- @param ply GPlayer The player to stop sending net messages to 301 | function WebAudio:Unsubscribe(ply) 302 | self.ignored:AddPlayer(ply) 303 | end 304 | 305 | --- Registers an object to send net messages to a chip after calling Unsubscribe on it. 306 | --- @param ply GPlayer The player to register to get messages again 307 | function WebAudio:Subscribe(ply) 308 | self.ignored:RemovePlayer(ply) 309 | end 310 | 311 | --- Returns if the player is subscribed to webaudio net messages. 312 | --- ## Warning 313 | --- Quite inefficient as it has to loop over a table under the scenes thanks to CRecipientFilter not having a function to check players. 314 | --- @param ply GPlayer The player to check 315 | --- @return boolean # Whether the player is subscribed 316 | function WebAudio:IsSubscribed(ply) 317 | return not table.HasValue( self.ignored:GetPlayers(), ply ) 318 | end 319 | 320 | --- Runs net.SendOmit for you, just broadcasts the webaudio to all players with the object enabled. 321 | -- Does ❌ broadcast to people who have destroyed the object on their client 322 | -- Does ❌ broadcast to people with wa_enable set to 0 323 | ---@param force_ignored boolean # Whether to also transmit to those who are ignored by it 324 | function WebAudio:Broadcast(force_ignored) 325 | -- Todo: Manually build this table because table.Add here is pretty bad for perf 326 | if force_ignored then 327 | net.SendOmit( StreamDisabledPlayers.__net ) 328 | else 329 | net.SendOmit( table.Add(self.ignored:GetPlayers(), StreamDisabledPlayers.__net) ) 330 | end 331 | end 332 | 333 | --- Stop sending net messages to players who want to ignore certain streams. 334 | net.Receive("wa_ignore", function(len, ply) 335 | if WebAudio.isSubscribed(ply) == false then return end -- They already have wa_enable set to 0 336 | local n_ignores = net.ReadUInt(8) 337 | 338 | for k = 1, n_ignores do 339 | local stream = WebAudio.readStream() 340 | if stream then 341 | stream:Unsubscribe(ply) 342 | end -- Doesn't exist anymore, maybe got removed when the net message was sending 343 | end 344 | end) 345 | 346 | --#endregion 347 | 348 | --#region Net Messages 349 | 350 | --- This receives net messages when the SERVER wants information about a webaudio stream you created. 351 | -- It gets stuff like average bitrate and length of the song this way. 352 | -- You'll only be affecting your own streams and chip, so there's no point in "abusing" this. 353 | net.Receive("wa_info", function(len, ply) 354 | local stream = WebAudio.readStream() 355 | if stream and stream.needs_info and stream.owner == ply then 356 | -- Make sure the stream exists, hasn't already received client info & That the net message sender is the owner of the WebAudio object. 357 | if net.ReadBool() then 358 | -- Failed to create. Doesn't support 3d, or is block streamed. 359 | stream:Destroy() 360 | else 361 | local continuous = net.ReadBool() 362 | local length = -2 363 | if not continuous then 364 | length = net.ReadUInt(16) 365 | end 366 | 367 | local file_name = net.ReadString() 368 | stream.length = length 369 | stream.filename = file_name 370 | stream.needs_info = false 371 | 372 | local watch = stream.stopwatch 373 | watch:SetDuration(length) 374 | -- watch:SetRate(stream.playback_rate) 375 | -- watch:SetTime(stream.time) 376 | end 377 | end 378 | end) 379 | 380 | net.Receive("wa_enable", function(len, ply) 381 | if net.ReadBool() then 382 | WebAudio.subscribe(ply) 383 | else 384 | WebAudio.unsubscribe(ply) 385 | end 386 | end) 387 | 388 | 389 | -- FFT will be an array of UInts (check FFTSAMP_LEN). It may not be fully filled to 64 samples, some may be nil to conserve bandwidth. Keep this in mind. 390 | -- We don't have to care for E2 since e2 automatically gives you 0 instead of nil for an error. 391 | net.Receive("wa_fft", function(len, ply) 392 | local stream = WebAudio.readStream() 393 | 394 | if stream and stream.owner == ply then 395 | local samp_len = WebAudio.FFTSAMP_LEN 396 | local samples = (len - WebAudio.ID_LEN) / samp_len 397 | local t = stream.fft 398 | for i = 1, 64 do 399 | t[i] = i > samples and 0 or net.ReadUInt(samp_len) 400 | end 401 | end 402 | end) 403 | 404 | --#endregion 405 | 406 | hook.Add("PlayerDisconnected", "wa_player_cleanup", function(ply) 407 | if ply:IsBot() then return end 408 | table.RemoveByValue(StreamDisabledPlayers.__net, ply) 409 | StreamDisabledPlayers.__hash[ply] = nil 410 | end) 411 | 412 | hook.Add("PlayerInitialSpawn", "wa_player_init", function(ply, transition) 413 | if ply:IsBot() then return end 414 | 415 | if ply:GetInfoNum("wa_enable", 1) == 0 then 416 | WebAudio.unsubscribe(ply) 417 | end 418 | end) 419 | 420 | --#region Static Functions 421 | 422 | ---@class WebAudio 423 | local WebAudioStatic = WebAudio.getStatics() 424 | 425 | --- Unsubscribe a player from receiving WebAudio net messages 426 | -- Like the non-static method but for all future Streams & Messages 427 | --- @param ply GPlayer Player to check 428 | function WebAudioStatic.unsubscribe(ply) 429 | if WebAudio.isSubscribed(ply) then 430 | StreamDisabledPlayers.__hash[ply] = true 431 | table.insert(StreamDisabledPlayers.__net, ply) 432 | end 433 | end 434 | 435 | --- Resubscribe a player to receive WebAudio net messages 436 | --- @param ply GPlayer Player to subscribe 437 | function WebAudioStatic.subscribe(ply) 438 | if not WebAudio.isSubscribed(ply) then 439 | StreamDisabledPlayers.__hash[ply] = false 440 | table.RemoveByValue(StreamDisabledPlayers.__net, ply) 441 | end 442 | end 443 | 444 | --- Returns whether the player is subscribed to receive WebAudio net messages or not. 445 | --- @param ply GPlayer Player to check 446 | --- @return boolean # If the player is subscribed. 447 | function WebAudioStatic.isSubscribed(ply) 448 | return not StreamDisabledPlayers.__hash[ply] 449 | end 450 | 451 | --#endregion 452 | 453 | concommand.Add("wa_purge", function() 454 | for _, stream in WebAudio.getIterator() do 455 | stream:Destroy(true) 456 | end 457 | end, nil, "Purges all of the currently playing WebAudio streams", 0) -------------------------------------------------------------------------------- /lua/autorun/webaudio.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Common Stuff 3 | (part 1) 4 | ]] 5 | 6 | --- x86 branch is on LuaJIT 2.0.4 which doesn't support binary literals 7 | local function b(s) 8 | return tonumber(s, 2) 9 | end 10 | 11 | -- Modification Flags 12 | -- If you ever change a setting of the Interface object, will add one of these flags to it. 13 | -- This will be sent to the client to know what to read in the net message to save networking 14 | local Modify = { 15 | volume = b"000000000001", 16 | time = b"000000000010", 17 | pos = b"000000000100", 18 | playing = b"000000001000", 19 | playback_rate = b"000000010000", 20 | direction = b"000000100000", 21 | parented = b"000001000000", 22 | radius = b"000010000000", 23 | looping = b"000100000000", 24 | mode = b"001000000000", 25 | reserved = b"010000000000", -- pan maybe? 26 | 27 | destroyed = b"100000000000", 28 | 29 | all = b"111111111111" 30 | } 31 | 32 | -- Bits needed to send a bitflag of all the modifications 33 | local MODIFY_LEN = math.ceil( math.log(Modify.all, 2) ) 34 | 35 | -- Max 1024 current streams 36 | local ID_LEN = math.ceil( math.log(1024, 2) ) 37 | 38 | local FFTSAMP_LEN = 8 39 | 40 | local function toBinary(n, bits) 41 | bits = bits or 32 42 | 43 | local t = {} 44 | for i = bits, 1, -1 do 45 | -- Basically go by each bit and use bit32.extract. Inlined the function here with width as 1. 46 | -- https://github.com/AlberTajuelo/bitop-lua/blob/8146f0b323f55f72eacbb2a8310922d50ae75ddf/src/bitop/funcs.lua#L92 47 | t[bits - i] = bit.band(bit.rshift(n, i), 1) 48 | end 49 | 50 | return "0b" .. table.concat(t) 51 | end 52 | 53 | local function hasModifyFlag(first, ...) 54 | return bit.band(first, ...) ~= 0 55 | end 56 | 57 | --- Debug function that's exported as well 58 | local function getFlags(number) 59 | local out = {} 60 | for enum_name, enum_val in pairs(Modify) do 61 | if hasModifyFlag(number, enum_val) then 62 | table.insert(out, enum_name) 63 | end 64 | end 65 | return out 66 | end 67 | 68 | -- SERVER 69 | local WAAdminOnly = CreateConVar("wa_admin_only", "0", FCVAR_REPLICATED, "Whether creation of WebAudio objects should be limited to admins. 0 for everyone, 1 for admins, 2 for superadmins. wa_enable_sv takes precedence over this", 0, 2) 70 | local WASCCompat = CreateConVar("wa_sc_compat", "0", FCVAR_ARCHIVE, "Whether streamcore-compatible functions should be generated for E2.", 0, 1) 71 | 72 | -- Max in total is ~1023 from ID_LEN writing a 10 bit uint. Assuming ~30 players max using webaudio, can only give ~30. 73 | local WAMaxStreamsPerUser = CreateConVar("wa_stream_max", "5", FCVAR_REPLICATED, "Max number of streams a player can have at once.", 1, 30) 74 | 75 | -- SHARED 76 | local WAEnabled = CreateConVar("wa_enable", "1", FCVAR_ARCHIVE + FCVAR_USERINFO, "Whether webaudio should be enabled to play on your client/server or not.", 0, 1) 77 | local WAMaxVolume = CreateConVar("wa_volume_max", "300", FCVAR_ARCHIVE, "Highest volume a webaudio sound can be played at, in percentage. 200 is 200%. SHARED Convar", 0, 100000) 78 | local WAMaxRadius = CreateConVar("wa_radius_max", "3000", FCVAR_ARCHIVE, "Farthest distance a WebAudio stream can be heard from. Will clamp to this value. SHARED Convar", 0, 1000000) 79 | local WAFFTEnabled = CreateConVar("wa_fft_enable", "1", FCVAR_ARCHIVE, "Whether FFT data is enabled for the server / your client. You shouldn't need to disable it as it is very lightweight.", 0, 1) 80 | 81 | -- CLIENT 82 | 83 | -- 0 is no prints, 1 is warnings/errors, 2 is additional logging 84 | local WAVerbosity 85 | if CLIENT then 86 | WAVerbosity = CreateConVar("wa_verbosity", "1", FCVAR_ARCHIVE, "Verbosity of WebAudio information & warnings printed to your console. 2 is warnings & stream logging, 1 is only warnings (default), 0 is nothing (Not recommended).", 0, 2) 87 | end 88 | 89 | local Black = Color(0, 0, 0, 255) 90 | local Color_Warn = Color(243, 71, 41, 255) 91 | local Color_Notify = Color(65, 172, 235, 255) 92 | local White = Color(255, 255, 255, 255) 93 | 94 | local function warn(...) 95 | local msg = string.format(...) 96 | MsgC(Black, "[", Color_Warn, "WA", Black, "]", White, ": ", msg, "\n") 97 | end 98 | 99 | local function notify(...) 100 | local msg = string.format(...) 101 | MsgC(Black, "[", Color_Notify, "WA", Black, "]", White, ": ", msg, "\n") 102 | end 103 | 104 | if WebAudio and WebAudio.disassemble then 105 | if SERVER then 106 | -- Make sure to also run wire_expression2_reload (after this) if working on this addon with hot reloading. 107 | notify("Reloaded!") 108 | end 109 | WebAudio.disassemble() 110 | end 111 | 112 | --[[ 113 | OOP 114 | ]] 115 | 116 | --- Initiate WebAudio struct for both realms 117 | ---@class WebAudio 118 | ---@field url string # SHARED 119 | ---@field stopwatch Stopwatch # SERVER 120 | ---@field radius number # SHARED 121 | ---@field radius_sqr number # SHARED 122 | ---@field looping boolean # SHARED 123 | ---@field parent GEntity # SHARED 124 | ---@field parented boolean # SHARED 125 | ---@field parent_pos GVector # CLIENT 126 | ---@field volume number # SHARED 0-1 127 | ---@field fft table # SERVER. Array of numbers 0-255 representing the FFT data. 128 | ---@field bass GIGModAudioChannel # CLIENT. Bass channel. 129 | ---@field filename string # SHARED. Filename of the sound. Not necessarily URL. Requires client to network it first. 130 | ---@field length integer # SHARED. Playtime length of the sound. Requires client to network it first. 131 | ---@field owner GEntity # SERVER. Owner of the webaudio or NULL for lua-owned. 132 | ---@field pos GVector # SERVER. Position of stream 133 | ---@field id integer # Custom ID for webaudio stream allocated between 0-MAX 134 | ---@field ignored GCRecipientFilter # Players to ignore when sending net messages. 135 | ---@field mode WebAudioMode 136 | ---@field hook_destroy table # SHARED. Hooks to run before/when the stream is destroyed. 137 | _G.WebAudio = {} 138 | WebAudio.__index = WebAudio 139 | 140 | WebAudio.MODIFY_LEN = MODIFY_LEN 141 | WebAudio.ID_LEN = ID_LEN 142 | WebAudio.FFTSAMP_LEN = FFTSAMP_LEN 143 | 144 | ---@alias WebAudioMode 0|1 145 | 146 | ---@type WebAudioMode 147 | WebAudio.MODE_2D = 0 148 | ---@type WebAudioMode 149 | WebAudio.MODE_3D = 1 150 | 151 | local WebAudioCounter = 0 152 | local WebAudios = {} -- TODO: See why weak kv doesn't work clientside for this 153 | 154 | local MaxID = 1023 -- UInt10. Only 1024 concurrent webaudio streams are allowed, if you use more than that then you've probably already broken the server 155 | local TopID = -1 -- Current top-most id. Set to -1 since we will increment first, having the ids start at 0. 156 | local IsSequential = true -- Boost for initial creation when no IDs have been dropped. 157 | 158 | local function register(id) 159 | -- id is set as occupied in the constructor through WebAudios 160 | WebAudioCounter = WebAudioCounter + 1 161 | TopID = id 162 | return id 163 | end 164 | 165 | --- Alloc an id for a webaudio stream & register it to WebAudios 166 | -- Modified from https://gist.github.com/Vurv78/2de64f77d04eb409abed008ab000e5ef 167 | --- @return integer? # ID or nil if reached max. 168 | local function allocID() 169 | if WebAudioCounter > MaxID then return end 170 | 171 | if IsSequential then 172 | return register(TopID + 1) 173 | end 174 | 175 | local left_until_loop = MaxID - TopID 176 | 177 | -- Avoid constant modulo with two for loops 178 | 179 | for i = 1, left_until_loop do 180 | local i2 = TopID + i 181 | if not WebAudios[i2] then 182 | return register(i2) 183 | end 184 | end 185 | 186 | for i = 0, TopID do 187 | if not WebAudios[i] then 188 | return register(i) 189 | end 190 | end 191 | end 192 | 193 | debug.getregistry()["WebAudio"] = WebAudio 194 | 195 | --- Returns whether the WebAudio object is valid and not destroyed. 196 | --- @return boolean # If it's Valid 197 | function WebAudio:IsValid() 198 | return not self.destroyed 199 | end 200 | 201 | --- Returns whether the WebAudio object is Null 202 | --- @return boolean # Whether it's destroyed/null 203 | function WebAudio:IsDestroyed() 204 | return self.destroyed 205 | end 206 | 207 | --- Calls the given function right before webaudio destruction. 208 | ---@param fun fun(self: WebAudio) 209 | function WebAudio:OnDestroy(fun) 210 | self.hook_destroy[fun] = true 211 | end 212 | 213 | --- Destroys a webaudio object and makes it the same (value wise) as WebAudio.getNULL() 214 | --- @param transmit boolean? If SERVER, should we transmit the destruction to the client? Default true 215 | function WebAudio:Destroy(transmit) 216 | if self:IsDestroyed() then return end 217 | if transmit == nil then transmit = true end 218 | 219 | for callback in pairs(self.hook_destroy) do 220 | callback(self) 221 | end 222 | 223 | if CLIENT and self.bass then 224 | self.bass:Stop() 225 | elseif transmit then 226 | if self.AddModify then 227 | -- Bandaid fix for garbage with autorefresh 228 | -- 1. create stream 229 | -- 2. wire_expression2_reload 230 | -- 3. autorefresh this file 231 | -- AddModify won't exist. 232 | self:AddModify(Modify.destroyed) 233 | self:Transmit(true) 234 | end 235 | end 236 | 237 | local id = self.id 238 | if WebAudios[id] then 239 | -- Drop ID 240 | WebAudioCounter = WebAudioCounter - 1 241 | IsSequential = false 242 | 243 | WebAudios[id] = nil 244 | end 245 | 246 | for k in next, self do 247 | self[k] = nil 248 | end 249 | self.destroyed = true 250 | 251 | return true 252 | end 253 | 254 | --- Returns current time in URL stream. 255 | -- Time elapsed is calculated on the server using playback rate and playback time. 256 | -- Not perfect for the clients and there will be desync if you pause and unpause constantly. 257 | --- @return number # Elapsed time 258 | function WebAudio:GetTimeElapsed() 259 | if self:IsDestroyed() then return -1 end 260 | return self.stopwatch:GetTime() 261 | end 262 | 263 | --- Returns the volume of the object set by SetVolume 264 | --- @return number # Volume from 0-1 265 | function WebAudio:GetVolume() 266 | return self.volume 267 | end 268 | 269 | --- Returns the position of the WebAudio object. 270 | --- @return GVector? # Position of the stream or nil if not set. 271 | function WebAudio:GetPos() 272 | return self.pos 273 | end 274 | 275 | --- Returns the radius of the stream set by SetRadius 276 | --- @return number # Radius 277 | function WebAudio:GetRadius() 278 | return self.radius 279 | end 280 | 281 | --- Returns the playtime length of a WebAudio object. 282 | --- @return number # Playtime Length 283 | function WebAudio:GetLength() 284 | return self.length 285 | end 286 | 287 | --- Returns the file name of the WebAudio object. Not necessarily always the URL. 288 | --- @return string # File name 289 | function WebAudio:GetFileName() 290 | return self.filename 291 | end 292 | 293 | --- Returns the state of the WebAudio object 294 | --- @return number # State, See STOPWATCH_* Enums 295 | function WebAudio:GetState() 296 | if self:IsDestroyed() then return STOPWATCH_STOPPED end 297 | return self.stopwatch:GetState() 298 | end 299 | 300 | --- Returns whether the webaudio stream is looping (Set by SetLooping.) 301 | --- @return boolean # Looping 302 | function WebAudio:GetLooping() 303 | return self.looping 304 | end 305 | 306 | --- Returns whether the webaudio stream is 3D enabled (Set by Set3DEnabled.) 307 | --- @return boolean # 3D Enabled 308 | function WebAudio:Get3DEnabled() 309 | return self.mode == WebAudio.MODE_3D 310 | end 311 | 312 | --- Returns whether the stream is parented or not. If it is parented, you won't be able to set it's position. 313 | --- @return boolean # # Whether it's parented 314 | function WebAudio:IsParented() 315 | return self.parented 316 | end 317 | 318 | -- Static Methods 319 | ---@class WebAudio 320 | local WebAudioStatic = {} 321 | -- For consistencies' sake, Static functions will be lowerCamelCase, while object / meta methods will be CamelCase. 322 | 323 | --- Returns an unusable WebAudio object that just has the destroyed field set to true. 324 | --- @return WebAudio # The null stream 325 | function WebAudioStatic.getNULL() 326 | return setmetatable({ destroyed = true }, WebAudio) 327 | end 328 | 329 | --- Same as net.ReadUInt but doesn't need the bit length in order to adapt our code more easily. 330 | ---- @return number # # UInt10 331 | function WebAudioStatic.readID() 332 | return net.ReadUInt(ID_LEN) 333 | end 334 | 335 | --- Faster version of WebAudio.getFromID( WebAudio.readID() ) 336 | ---- @return WebAudio? # # The stream or nil if it wasn't found 337 | function WebAudioStatic.readStream() 338 | return WebAudios[ net.ReadUInt(ID_LEN) ] 339 | end 340 | 341 | --- Same as net.WriteUInt but doesn't need the bit length in order to adapt our code more easily. 342 | ---- @param number id UInt10 (max id is 1023) 343 | function WebAudioStatic.writeID(id) 344 | net.WriteUInt(id, ID_LEN) 345 | end 346 | 347 | --- Same as net.WriteUInt but doesn't need the bit length in order to adapt our code more easily. 348 | ---- @param WebAudio stream The stream. This is the same as WebAudio.writeID( stream.id ) 349 | function WebAudioStatic.writeStream(stream) 350 | net.WriteUInt(stream.id, ID_LEN) 351 | end 352 | 353 | --- Reads a WebAudio Modify enum 354 | ---- @return number # UInt16 355 | function WebAudioStatic.readModify() 356 | return net.ReadUInt(MODIFY_LEN) 357 | end 358 | 359 | --- Writes a WebAudio Modify enum 360 | ---- @param number modify UInt16 361 | function WebAudioStatic.writeModify(modify) 362 | net.WriteUInt(modify, MODIFY_LEN) 363 | end 364 | 365 | ---@return WebAudio 366 | function WebAudioStatic.getFromID(id) 367 | return WebAudios[id] 368 | end 369 | 370 | function WebAudioStatic.getList() 371 | return WebAudios 372 | end 373 | 374 | local next = next 375 | 376 | --- Returns an iterator for the global table of all webaudio streams. Use this in a for in loop 377 | --- @return fun(): number, WebAudio # Iterator, should be the same as ipairs / next 378 | --- @return table # Global webaudio table. Same as WebAudio.getList() 379 | --- @return number # Origin index, will always be nil (0 would be ipairs) 380 | function WebAudioStatic.getIterator() 381 | return next, WebAudios, nil 382 | end 383 | 384 | --- Returns the number of currently active streams 385 | -- Cheaper than manually calling __len on the webaudio table as we keep track of it ourselves. 386 | ---@return integer 387 | function WebAudioStatic.getCountActive() 388 | return WebAudioCounter 389 | end 390 | 391 | --- Returns if an object is a WebAudio object, even if Invalid/Destroyed. 392 | ---@return boolean? 393 | local function isWebAudio(v) 394 | if not istable(v) then return end 395 | return debug.getmetatable(v) == WebAudio 396 | end 397 | 398 | WebAudioStatic.instanceOf = isWebAudio 399 | 400 | --- If you want to extend the static functions 401 | --- @return table # The WebAudio static function / var table. 402 | function WebAudioStatic.getStatics() 403 | return WebAudioStatic 404 | end 405 | 406 | --- Used internally. Should be called both server and client as it doesn't send any net messages to destroy the ids to the client. 407 | -- Called on WebAudio reload to stop all streams 408 | function WebAudioStatic.disassemble() 409 | for _, stream in WebAudio.getIterator() do 410 | stream:Destroy(false) 411 | end 412 | end 413 | 414 | local function createWebAudio(_, url, owner, bassobj, id) 415 | assert( WebAudio.isWhitelistedURL(url), "'url' argument must be given a whitelisted url string." ) 416 | -- assert( owner and isentity(owner) and owner:IsPlayer(), "'owner' argument must be a valid player." ) 417 | -- Commenting this out in case someone wants a webaudio object to be owned by the world or something. 418 | 419 | local self = setmetatable({ 420 | -- allocID will return nil if there's no slots left. 421 | id = assert( id or allocID(), "Reached maximum amount of concurrent WebAudio streams!" ), 422 | url = url, 423 | owner = owner, 424 | mode = WebAudio.MODE_3D, 425 | 426 | --#region mutable 427 | playing = false, 428 | destroyed = false, 429 | 430 | parented = false, 431 | parent = nil, -- Entity 432 | 433 | modified = 0, 434 | playback_rate = 1, 435 | volume = 1, 436 | time = 0, 437 | 438 | radius = math.min(200, WAMaxRadius:GetInt()), -- Default IGmodAudioChannel radius 439 | radius_sqr = math.min(200, WAMaxRadius:GetInt()) ^ 2, 440 | 441 | pos = nil, 442 | direction = Vector(0, 0, 0), 443 | looping = false, 444 | --#endregion mutable 445 | 446 | --#region net vars 447 | needs_info = SERVER, -- Whether this stream still needs information from the client. 448 | length = -1, 449 | filename = "", 450 | fft = {}, 451 | 452 | hook_destroy = {} 453 | --#endregion net vars 454 | }, WebAudio) 455 | 456 | 457 | if CLIENT then 458 | self.bass = bassobj 459 | self.parent_pos = Vector(0, 0, 0) -- Parent pos being nil means we will go directly to the parent's position w/o calculating local pos. 460 | else 461 | -- Stream will be set to 100 second length until the length of the audio stream is determined by the client. 462 | self.stopwatch = StopWatch.new(100, function(watch) 463 | if not watch:GetLooping() then 464 | self:Pause() 465 | end 466 | end) 467 | 468 | self.ignored = RecipientFilter() 469 | 470 | net.Start("wa_create", true) 471 | WebAudio.writeID(self.id) 472 | net.WriteString(self.url) 473 | net.WriteEntity(self.owner) 474 | self:Broadcast(false) -- Defined in wa_interface 475 | end 476 | 477 | WebAudios[self.id] = self 478 | 479 | return self 480 | end 481 | 482 | setmetatable(WebAudio, { 483 | __index = WebAudioStatic, 484 | __call = createWebAudio 485 | }) 486 | 487 | --- Creates a new webaudio object. Prefer this over the __call version 488 | ---@param url string 489 | ---@param owner GPlayer? 490 | ---@param bassobj GIGModAudioChannel? 491 | ---@param id number? 492 | ---@return WebAudio 493 | function WebAudio.new(url, owner, bassobj, id) 494 | return createWebAudio(WebAudio, url, owner, bassobj, id) 495 | end 496 | 497 | --[[ 498 | Whitelist handling 499 | ]] 500 | 501 | -- Match, IsPattern 502 | local function pattern(str) return { str, true } end 503 | local function simple(str) return { string.PatternSafe(str), false } end 504 | local registers = { ["pattern"] = pattern, ["simple"] = simple } 505 | 506 | --[[ 507 | Inspired / Taken from StarfallEx & Metastruct/gurl 508 | No blacklist for now, just don't whitelist anything that has any possible redir routes. 509 | 510 | ## Guidelines 511 | Sites cannot track users / do any scummy shit with your data unless they're a massive corporation that you really can't avoid anyways. 512 | So don't think about PRing your own website 513 | Also these have to do with audio since this is an audio based addon. 514 | 515 | ## Custom whitelist 516 | Create a file called webaudio_whitelist.txt in your data folder to overwrite this, works on the server box or on your client. 517 | Example file might look like this: 518 | ``` 519 | pattern %w+%.sndcdn%.com 520 | simple translate.google.com 521 | ``` 522 | ]]-- 523 | 524 | local Whitelist = { 525 | -- Soundcloud 526 | pattern [[[%w-_]+%.sndcdn%.com/.+]], 527 | 528 | -- Bandcamp 529 | pattern [[[%w-_]+%.bcbits%.com/.+]], 530 | 531 | -- Google Video (used by Youtube) 532 | pattern [[[%w-_]+%.googlevideo%.com/.+]], 533 | 534 | -- Google Translate Api, Needs an api key. 535 | simple [[translate.google.com]], 536 | 537 | -- Discord ( Disabled because discord blocked steam http headers :\ ) 538 | -- pattern [[cdn[%w-_]*%.discordapp%.com/.+]], 539 | 540 | -- Reddit 541 | simple [[i.redditmedia.com]], 542 | simple [[i.redd.it]], 543 | simple [[preview.redd.it]], 544 | 545 | -- Shoutcast 546 | simple [[yp.shoutcast.com]], 547 | 548 | -- Dropbox 549 | simple [[dl.dropboxusercontent.com]], 550 | pattern [[%w+%.dl%.dropboxusercontent%.com/(.+)]], 551 | simple [[dropbox.com]], 552 | simple [[dl.dropbox.com]], 553 | 554 | -- Github 555 | simple [[raw.githubusercontent.com]], 556 | simple [[gist.githubusercontent.com]], 557 | simple [[raw.github.com]], 558 | simple [[cloud.githubusercontent.com]], 559 | 560 | -- Steam 561 | simple [[steamuserimages-a.akamaihd.net]], 562 | simple [[steamcdn-a.akamaihd.net]], 563 | 564 | -- Gitlab 565 | simple [[gitlab.com]], 566 | 567 | -- Onedrive 568 | simple [[onedrive.live.com/redir]], 569 | simple [[api.onedrive.com]], 570 | 571 | -- ytdl host. Requires 2d mode which we currently don't support. 572 | simple [[youtubedl.mattjeanes.com]], 573 | 574 | -- MyInstants 575 | simple [[myinstants.com]], 576 | 577 | -- TTS like moonbase alpha's 578 | simple [[tts.cyzon.us]], 579 | 580 | -- US Air Traffic Control radio host 581 | simple [[liveatc.net]], 582 | 583 | -- Star Trek Sounds 584 | simple [[trekcore.com/audio]], 585 | 586 | -- Broadcastify 587 | pattern [[broadcastify%.cdnstream1%.com/%d+]], 588 | 589 | -- Google Drive 590 | simple [[docs.google.com/uc]], 591 | simple [[drive.google.com/uc]] 592 | } 593 | 594 | local OriginalWhitelist = table.Copy(Whitelist) 595 | local CustomWhitelist = false 596 | local LocalWhitelist = {} 597 | 598 | if SERVER then 599 | util.AddNetworkString("wa_sendcwhitelist") -- Receive server whitelist. 600 | 601 | local function sendCustomWhitelist(whitelist, ply) 602 | net.Start("wa_sendcwhitelist", false) 603 | if whitelist then 604 | net.WriteTable(whitelist) 605 | end 606 | if ply then 607 | net.Send(ply) 608 | else 609 | net.Broadcast() 610 | end 611 | end 612 | 613 | WebAudioStatic.sendCustomWhitelist = sendCustomWhitelist 614 | 615 | hook.Add("PlayerInitialSpawn", "wa_player_whitelist", function(ply) 616 | if ply:IsBot() then return end 617 | sendCustomWhitelist(Whitelist, ply) 618 | end) 619 | elseif CLIENT then 620 | net.Receive("wa_sendcwhitelist", function(len) 621 | if len == 0 then 622 | Whitelist = OriginalWhitelist 623 | WebAudio.Common.Whitelist = OriginalWhitelist 624 | else 625 | Whitelist = net.ReadTable() 626 | WebAudio.Common.Whitelist = Whitelist 627 | end 628 | end) 629 | end 630 | 631 | 632 | local function loadWhitelist(reloading) 633 | if file.Exists("webaudio_whitelist.txt", "DATA") then 634 | CustomWhitelist = true 635 | 636 | local dat = file.Read("webaudio_whitelist.txt", "DATA") 637 | local new_list, ind = {}, 0 638 | 639 | for line in dat:gmatch("[^\r\n]+") do 640 | local type, match = line:match("(%w+)%s+(.*)") 641 | local reg = registers[type] 642 | if reg then 643 | ind = ind + 1 644 | new_list[ind] = reg(match) 645 | elseif type ~= nil then 646 | -- Make sure type isn't nil so we ignore empty lines 647 | warn("Invalid entry type found [\"", type, "\"] in webaudio_whitelist\n") 648 | end 649 | end 650 | 651 | notify("Whitelist from webaudio_whitelist.txt found and parsed with %d entries!", ind) 652 | if SERVER then 653 | Whitelist = new_list 654 | WebAudio.Common.Whitelist = new_list 655 | WebAudio.sendCustomWhitelist(Whitelist) 656 | else 657 | LocalWhitelist = new_list 658 | end 659 | WebAudio.Common.CustomWhitelist = true 660 | return true 661 | elseif reloading then 662 | notify("Couldn't find your whitelist file! %s", CLIENT and "Make sure to run this on the server if you want to reload the server's whitelist!" or "") 663 | return false 664 | end 665 | return false 666 | end 667 | 668 | local function checkWhitelist(wl, relative) 669 | for _, data in ipairs(wl) do 670 | local match, is_pattern = data[1], data[2] 671 | 672 | local haystack = is_pattern and relative or (relative:match("(.-)/.*") or relative) 673 | if haystack:find( "^" .. match .. (is_pattern and "" or "$") ) then 674 | return true 675 | end 676 | end 677 | 678 | return false 679 | end 680 | 681 | local function isWhitelistedURL(url) 682 | if not isstring(url) then return false end 683 | url = url:Trim() 684 | 685 | local isWhitelisted = hook.Run("WA_IsWhitelistedURL", url) 686 | if isWhitelisted ~= nil then return isWhitelisted end 687 | 688 | local relative = url:match("^https?://www%.(.*)") or url:match("^https?://(.*)") 689 | if not relative then return false end 690 | 691 | if CLIENT and CustomWhitelist then 692 | return checkWhitelist(LocalWhitelist, relative) 693 | end 694 | 695 | return checkWhitelist(Whitelist, relative) 696 | end 697 | 698 | concommand.Add("wa_reload_whitelist", function() 699 | if not loadWhitelist() then 700 | Whitelist = OriginalWhitelist 701 | CustomWhitelist = false 702 | WebAudio.Common.CustomWhitelist = false 703 | WebAudio.sendCustomWhitelist() 704 | end 705 | end, nil, "Reload the whitelist", 0) 706 | 707 | concommand.Add("wa_list", function() 708 | local stream_list, n = {}, 0 709 | for id, stream in WebAudio.getIterator() do 710 | local owner = stream.owner 711 | if IsValid(owner) and owner:IsPlayer() then 712 | n = n + 1 713 | ---@diagnostic disable-next-line (Emmylua definitions don't cover the Player class well.) 714 | stream_list[n] = string.format("[%d] %s(%s): '%s'", stream.id, owner:GetName(), owner:SteamID64() or "multirun", stream.url) 715 | end 716 | end 717 | MsgC( White, "---------> WebAudio List <---------\n" ) 718 | if n == 0 then 719 | MsgC( White, "None!\n") 720 | else 721 | MsgC( White, table.concat(stream_list, '\n'), "\n" ) 722 | end 723 | end, nil, "List all currently playing WebAudio streams", 0) 724 | 725 | concommand.Add("wa_help", function() 726 | MsgC( White, "You can get help & report issues on the Github: ", Color_Notify, "https://github.com/Vurv78/WebAudio", White, "\n" ) 727 | end, nil, "Get help & report issues on the Github", 0) 728 | 729 | if CLIENT then 730 | local Color_Aqua = Color(60, 240, 220, 255) 731 | 732 | local function DrawOwners() 733 | for id, stream in WebAudio.getIterator() do 734 | local owner = stream.owner 735 | if owner and owner:IsValid() and stream.pos then 736 | local pos = stream.pos:ToScreen() 737 | ---@diagnostic disable-next-line (Emmylua definitions don't cover the Player class well.) 738 | local txt = string.format("[%u] %s (%s)", id, owner:GetName(), owner:SteamID64() or "multirun") 739 | draw.DrawText( txt, "DermaDefault", pos.x, pos.y, Color_Aqua, TEXT_ALIGN_CENTER) 740 | end 741 | end 742 | end 743 | 744 | local Display = false 745 | concommand.Add("wa_display", function() 746 | Display = not Display 747 | 748 | if Display then 749 | hook.Add("HUDPaint", "wa_draw_owners", DrawOwners) 750 | else 751 | hook.Remove("HUDPaint", "wa_draw_owners") 752 | end 753 | end, nil, "Draw owners of the currently playing WebAudio streams", 0) 754 | end 755 | 756 | WebAudioStatic.isWhitelistedURL = isWhitelistedURL 757 | 758 | WebAudio.Common = { 759 | -- Object 760 | WebAudio = WebAudio, 761 | 762 | -- Common 763 | warn = warn, 764 | notify = notify, 765 | 766 | Modify = Modify, 767 | hasModifyFlag = hasModifyFlag, 768 | toBinary = toBinary, 769 | getFlags = getFlags, 770 | 771 | --- Convars 772 | 773 | -- Server 774 | WAAdminOnly = WAAdminOnly, 775 | WAMaxStreamsPerUser = WAMaxStreamsPerUser, 776 | WASCCompat = WASCCompat, 777 | 778 | -- Shared 779 | WAEnabled = WAEnabled, 780 | WAMaxVolume = WAMaxVolume, 781 | WAMaxRadius = WAMaxRadius, 782 | WAFFTEnabled = WAFFTEnabled, 783 | 784 | -- Client 785 | WAVerbosity = WAVerbosity, 786 | 787 | -- Whitelist 788 | loadWhitelist = loadWhitelist, 789 | CustomWhitelist = CustomWhitelist, -- If we're using a user supplied whitelist. 790 | Whitelist = Whitelist, 791 | OriginalWhitelist = OriginalWhitelist 792 | } 793 | 794 | loadWhitelist() 795 | 796 | AddCSLuaFile("webaudio/receiver.lua") 797 | 798 | if CLIENT then 799 | include("webaudio/receiver.lua") 800 | else 801 | include("webaudio/interface.lua") 802 | end 803 | 804 | return WebAudio.Common 805 | -------------------------------------------------------------------------------- /lua/entities/gmod_wire_expression2/core/custom/webaudio.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | WebAudio E2 Library 3 | 4 | If you notice ``if condition then return 1 else return 0`` yes that is intentional. 5 | It should be more efficient than lua's 'ternary' 6 | ]] 7 | 8 | --#region prelude 9 | 10 | E2Lib.RegisterExtension("webaudio", true, "Adds 3D Bass/IGmodAudioChannel web streaming to E2.") 11 | 12 | local WebAudio = WebAudio 13 | if not WebAudio then 14 | WebAudio = include("autorun/webaudio.lua") 15 | end 16 | local Common = WebAudio.Common 17 | 18 | -- Convars 19 | local Enabled, AdminOnly, FFTEnabled, SCCompat = Common.WAEnabled, Common.WAAdminOnly, Common.WAFFTEnabled, Common.WASCCompat 20 | local MaxStreams, MaxVolume, MaxRadius = Common.WAMaxStreamsPerUser, Common.WAMaxVolume, Common.WAMaxRadius 21 | 22 | local StreamCounter = WireLib.RegisterPlayerTable() 23 | 24 | local CREATE_REGEN = 0.3 -- 300ms to regenerate a stream 25 | local NET_REGEN = 0.1 -- 100ms to regenerate net messages 26 | 27 | E2Lib.registerConstant( "CHANNEL_STOPPED", STOPWATCH_STOPPED ) 28 | E2Lib.registerConstant( "CHANNEL_PLAYING", STOPWATCH_PLAYING ) 29 | E2Lib.registerConstant( "CHANNEL_PAUSED", STOPWATCH_PAUSED ) 30 | E2Lib.registerConstant( "WA_FFT_SAMPLES", 64 ) -- Default samples # 31 | E2Lib.registerConstant( "WA_FFT_DELAY", 80 ) -- Delay in ms 32 | 33 | local function makeAlias(alias_name, original) 34 | local alias = table.Copy(wire_expression2_funcs[original]) 35 | alias[1] = alias_name 36 | wire_expression2_funcs[alias_name] = alias 37 | end 38 | 39 | registerType("webaudio", "xwa", WebAudio.getNULL(), 40 | function(self, input) 41 | return input 42 | end, 43 | function(self, output) 44 | return output 45 | end, 46 | function(ret) 47 | -- For some reason we don't throw an error here. 48 | -- See https://github.com/wiremod/wire/blob/501dd9875ab1f6db37a795e1f9a946d382db4f1f/lua/entities/gmod_wire_expression2/core/entity.lua#L10 49 | 50 | if not ret then return end 51 | if not WebAudio.instanceOf(ret) then 52 | error("Return value is neither nil nor a WebAudio object, but a %s!", type(ret)) 53 | end 54 | end, 55 | WebAudio.instanceOf 56 | ) 57 | 58 | local function registerStream(self, url, owner) 59 | local stream = WebAudio.new(url, owner) 60 | 61 | local idx = table.insert(self.data.webaudio_streams, stream) 62 | StreamCounter[owner] = (StreamCounter[owner] or 0) + 1 63 | 64 | stream:OnDestroy(function(_) 65 | self.data.webaudio_streams[idx] = nil 66 | StreamCounter[owner] = StreamCounter[owner] - 1 67 | end) 68 | 69 | return stream 70 | end 71 | 72 | --#endregion prelude 73 | 74 | --#region stringify 75 | 76 | -- string toString(webaudio stream) 77 | __e2setcost(2) 78 | registerFunction("toString", "xwa", "s", function(self, args) 79 | local op1 = args[2] 80 | ---@type WebAudio 81 | local stream = op1[1](self, op1) 82 | 83 | if IsValid(stream) then 84 | return string.format("WebAudio [%d]", stream.id) 85 | else 86 | return "WebAudio [null]" 87 | end 88 | end) 89 | 90 | makeAlias("toString(xwa:)", "toString(xwa)") 91 | 92 | WireLib.registerDebuggerFormat("WEBAUDIO", function(stream) 93 | if IsValid(stream) then 94 | return string.format( "WebAudio [Id: %d, volume: %d%%, time: %.02f/%d]", stream.id, stream.volume * 100, stream.stopwatch:GetTime(), stream.length ) 95 | else 96 | return "WebAudio [null]" 97 | end 98 | end) 99 | 100 | --#endregion stringify 101 | 102 | --#region operators 103 | 104 | registerOperator("is", "xwa", "n", function(self, args) -- if(Wa) 105 | local op1 = args[2] 106 | 107 | --- @type WebAudio 108 | local v1 = op1[1](self, op1) 109 | if v1 and v1:IsValid() then return 1 else return 0 end 110 | end) 111 | 112 | --#endregion operators 113 | 114 | --#region util 115 | 116 | --- Checks if a player / chip has permissions to use webaudio. 117 | --- @param self table 'self' from E2Functions. 118 | --- @param ent GEntity? Optional entity to check if they have permissions to modify. 119 | local function checkPermissions(self, ent) 120 | if not Enabled:GetBool() then E2Lib.raiseException("WebAudio is currently disabled on the server!", nil, self.trace) end 121 | local ply = self.player 122 | 123 | local required_lv = AdminOnly:GetInt() 124 | if required_lv == 1 then 125 | if not ply:IsAdmin() then E2Lib.raiseException("WebAudio is currently restricted to admins!", nil, self.trace) end 126 | elseif required_lv == 2 then 127 | if not ply:IsSuperAdmin() then E2Lib.raiseException("WebAudio is currently restricted to super-admins!", nil, self.trace) end 128 | end 129 | 130 | if ent and not E2Lib.isOwner(self, ent) then 131 | -- Checking if you have perms to modify this prop. 132 | E2Lib.raiseException("You do not have permissions to modify this prop!", nil, self.trace) 133 | end 134 | end 135 | 136 | --- Generic Burst limit until wiremod gets its own like Starfall. 137 | -- Also acts as a limit on the number of something. 138 | ---@class Burst 139 | ---@field max number 140 | ---@field regen number 141 | ---@field tracker table 142 | local Burst = {} 143 | Burst.__index = Burst 144 | 145 | ---@param max number 146 | ---@param regen_time number 147 | ---@return Burst 148 | function Burst.new(max, regen_time) 149 | return setmetatable({ 150 | max = max, -- Will start full. 151 | 152 | regen = regen_time, 153 | tracker = WireLib.RegisterPlayerTable() 154 | }, Burst) 155 | end 156 | 157 | function Burst:get(ply) 158 | local data = self.tracker[ply] 159 | if data then 160 | return data.stock 161 | else 162 | return self.max 163 | end 164 | end 165 | 166 | function Burst:check(ply) 167 | local data = self.tracker[ply] 168 | if data then 169 | local now = CurTime() 170 | local last = data.last 171 | 172 | local stock = data.stock 173 | if stock == 0 then 174 | stock = math.min( math.floor( (now - last) / self.regen), self.max) 175 | return stock > 0 176 | else 177 | return true 178 | end 179 | else 180 | return true 181 | end 182 | end 183 | 184 | ---@param ply GPlayer 185 | function Burst:use(ply) 186 | local data = self.tracker[ply] 187 | if data then 188 | local now = CurTime() 189 | local last = data.last 190 | 191 | local stock = math.min( data.stock + math.floor( (now - last) / self.regen), self.max ) 192 | 193 | if stock > 0 then 194 | data.stock = stock - 1 195 | data.last = CurTime() 196 | return true 197 | else 198 | return false 199 | end 200 | else 201 | self.tracker[ply] = { 202 | stock = self.max - 1, -- stockpile 203 | last = CurTime() 204 | } 205 | return true 206 | end 207 | end 208 | 209 | local CreationBurst = Burst.new( MaxStreams:GetInt(), CREATE_REGEN ) -- Prevent too many creations at once. (To avoid Creation->Destruction net spam.) 210 | local NetBurst = Burst.new( 10, NET_REGEN ) -- In order to prevent too many updates at once. 211 | 212 | cvars.RemoveChangeCallback("wa_stream_max", "wa_stream_max") 213 | 214 | cvars.AddChangeCallback("wa_stream_max", function(_cvar_name, _old, new) 215 | CreationBurst.max = new 216 | end, "wa_stream_max") 217 | 218 | local function checkCounter(ply) 219 | return (StreamCounter[ply] or 0) < MaxStreams:GetInt() 220 | end 221 | 222 | --#endregion util 223 | 224 | -- webaudio webAudio(string url) 225 | __e2setcost(50) 226 | registerFunction("webAudio", "s", "xwa", function(self, args) 227 | local op1 = args[2] 228 | local url = op1[1](self, op1) 229 | 230 | local ply = self.player 231 | checkPermissions(self) 232 | 233 | if not WebAudio.isWhitelistedURL(url) then 234 | if WebAudio.Common.CustomWhitelist then 235 | E2Lib.raiseException("This URL is not whitelisted! The server has a custom whitelist.", nil, self.trace) 236 | else 237 | E2Lib.raiseException("This URL is not whitelisted! See github.com/Vurv78/WebAudio/blob/main/WHITELIST.md", nil, self.trace) 238 | end 239 | end 240 | 241 | -- Creation Time Quota 242 | if not CreationBurst:use(ply) then 243 | E2Lib.raiseException("You are creating WebAudios too fast. Check webAudioCanCreate before calling!", nil, self.trace) 244 | end 245 | 246 | -- Stream Count Quota 247 | if not checkCounter(ply) then 248 | E2Lib.raiseException("Reached maximum amount of WebAudio streams! Check webAudioCanCreate or webAudiosLeft before calling!", nil, self.trace) 249 | end 250 | 251 | return registerStream(self, url, ply) 252 | end) 253 | 254 | --#region ability check 255 | 256 | -- number webAudioCanCreate() 257 | __e2setcost(2) 258 | registerFunction("webAudioCanCreate", "", "n", function(self, args) 259 | local ply = self.player 260 | 261 | return ( 262 | CreationBurst:check(ply) 263 | and checkCounter(ply) 264 | ) and 1 or 0 265 | end) 266 | 267 | -- number webAudioCanCreate(string url) 268 | __e2setcost(4) 269 | registerFunction("webAudioCanCreate", "s", "n", function(self, args) 270 | local op1 = args[2] 271 | local url = op1[1](self, op1) 272 | 273 | local ply = self.player 274 | 275 | return ( 276 | CreationBurst:check(ply) 277 | and checkCounter(ply) 278 | and WebAudio.isWhitelistedURL(url) 279 | ) and 1 or 0 280 | end) 281 | 282 | -- number webAudioEnabled() 283 | __e2setcost(1) 284 | registerFunction("webAudioEnabled", "", "n", function(self, args) 285 | return Enabled:GetInt() 286 | end) 287 | 288 | -- number webAudioAdminOnly() 289 | registerFunction("webAudioAdminOnly", "", "n", function(self, args) 290 | return AdminOnly:GetInt() 291 | end) 292 | 293 | -- number webAudiosLeft() 294 | registerFunction("webAudiosLeft", "", "n", function(self, args) 295 | return MaxStreams:GetInt() - (StreamCounter[self.player] or 0) 296 | end) 297 | 298 | -- number webAudioCanTransmit() 299 | registerFunction("webAudioCanTransmit", "", "n", function(self, args) 300 | if NetBurst:check(self.player) then return 1 else return 0 end 301 | end) 302 | 303 | --#endregion ability check 304 | 305 | -- number webaudio:isValid() 306 | __e2setcost(4) 307 | registerFunction("isValid", "xwa:", "n", function(self, args) 308 | local op1 = args[2] 309 | ---@type WebAudio 310 | local this = op1[1](self, op1) 311 | 312 | if this and this:IsValid() then return 1 else return 0 end 313 | end) 314 | 315 | -- webaudio nowebaudio() 316 | __e2setcost(2) 317 | registerFunction("nowebaudio", "", "xwa", WebAudio.getNULL) 318 | 319 | --#region special 320 | 321 | -- number webaudio:play() 322 | __e2setcost(15) 323 | registerFunction("play", "xwa:", "n", function(self, args) 324 | local op1 = args[2] 325 | ---@type WebAudio 326 | local this = op1[1](self, op1) 327 | 328 | checkPermissions(self) 329 | if not NetBurst:use(self.player) then return self:throw("You are transmitting too fast, check webAudioCanTransmit!", 0) end 330 | 331 | if this.did_set_pos == nil and not IsValid(this.parent) then 332 | -- They didn't set position nor parent it. 333 | -- Probably want to default to parenting on chip. 334 | this:SetParent( self.entity ) 335 | end 336 | 337 | return this:Play() and 1 or 0 338 | end) 339 | 340 | -- number webaudio:pause() 341 | registerFunction("pause", "xwa:", "n", function(self, args) 342 | local op1 = args[2] 343 | ---@type WebAudio 344 | local this = op1[1](self, op1) 345 | 346 | checkPermissions(self) 347 | if not NetBurst:use(self.player) then return self:throw("You are transmitting too fast, check webAudioCanTransmit!", 0) end 348 | 349 | if this:Pause() then return 1 else return 0 end 350 | end) 351 | 352 | -- void webaudio:destroy() 353 | registerFunction("destroy", "xwa:", "n", function(self, args) 354 | local op1 = args[2] 355 | ---@type WebAudio 356 | local this = op1[1](self, op1) 357 | -- No limit here because they'd already have been limited by the creation burst. 358 | return this:Destroy() and 1 or 0 359 | end) 360 | 361 | -- void webaudio:update() 362 | registerFunction("update", "xwa:", "n", function(self, args) 363 | local op1 = args[2] 364 | ---@type WebAudio 365 | local this = op1[1](self, op1) 366 | 367 | checkPermissions(self) 368 | if not NetBurst:use(self.player) then return self:throw("You are transmitting too fast, check webAudioCanTransmit!", 0) end 369 | 370 | if this:IsValid() then 371 | return this:Transmit(false) and 1 or 0 372 | else 373 | return self:throw("This WebAudio is not valid!", 0) 374 | end 375 | end) 376 | 377 | --#endregion special 378 | 379 | --#region setters 380 | 381 | -- void webaudio:setPos(vector pos) 382 | __e2setcost(5) 383 | registerFunction("setPos", "xwa:v", "n", function(self, args) 384 | local op1, op2 = args[2], args[3] 385 | ---@type WebAudio 386 | local this = op1[1](self, op1) 387 | ---@type GVector 388 | local pos = op2[1](self, op2) 389 | 390 | checkPermissions(self) 391 | 392 | if this:IsValid() then 393 | this.did_set_pos = true 394 | this:SetPos(pos) 395 | else 396 | return self:throw("Invalid WebAudio instance!", 0) 397 | end 398 | end) 399 | 400 | -- void webaudio:setVolume(number vol) 401 | __e2setcost(5) 402 | registerFunction("setVolume", "xwa:n", "n", function(self, args) 403 | local op1, op2 = args[2], args[3] 404 | ---@type WebAudio 405 | local this = op1[1](self, op1) 406 | local volume = op2[1](self, op2) 407 | 408 | checkPermissions(self) 409 | 410 | if this:IsValid() then 411 | return this:SetVolume( math.min(volume, MaxVolume:GetInt()) / 100 ) and 1 or 0 412 | else 413 | return self:throw("Invalid WebAudio instance!", 0) 414 | end 415 | end) 416 | 417 | -- number webaudio:setTime(number time) 418 | registerFunction("setTime", "xwa:n", "n", function(self, args) 419 | local op1, op2 = args[2], args[3] 420 | ---@type WebAudio 421 | local this = op1[1](self, op1) 422 | local time = op2[1](self, op2) 423 | 424 | checkPermissions(self) 425 | 426 | if this:IsValid() then 427 | return this:SetTime(time) and 1 or 0 428 | else 429 | return self:throw("Invalid WebAudio instance!", 0) 430 | end 431 | end) 432 | 433 | -- void webaudio:setPlaybackRate(number rate) 434 | registerFunction("setPlaybackRate", "xwa:n", "n", function(self, args) 435 | local op1, op2 = args[2], args[3] 436 | ---@type WebAudio 437 | local this = op1[1](self, op1) 438 | local rate = op2[1](self, op2) 439 | 440 | checkPermissions(self) 441 | 442 | if this:IsValid() then 443 | return this:SetPlaybackRate(rate) and 1 or 0 444 | else 445 | return self:throw("Invalid WebAudio instance!", 0) 446 | end 447 | end) 448 | 449 | registerFunction("setDirection", "xwa:v", "n", function(self, args) 450 | local op1, op2 = args[2], args[3] 451 | ---@type WebAudio 452 | local this = op1[1](self, op1) 453 | local dir = op2[1](self, op2) 454 | 455 | checkPermissions(self) 456 | 457 | if this:IsValid() then 458 | return this:SetPlaybackRate(dir) and 1 or 0 459 | else 460 | return self:throw("Invalid WebAudio instance!", 0) 461 | end 462 | end) 463 | 464 | -- void webaudio:setRadius(number radius) 465 | registerFunction("setRadius", "xwa:n", "n", function(self, args) 466 | local op1, op2 = args[2], args[3] 467 | ---@type WebAudio 468 | local this = op1[1](self, op1) 469 | local radius = op2[1](self, op2) 470 | 471 | checkPermissions(self) 472 | 473 | if this:IsValid() then 474 | return this:SetRadius( math.min(radius, MaxRadius:GetInt()) ) and 1 or 0 475 | else 476 | return self:throw("Invalid WebAudio instance!", 0) 477 | end 478 | end) 479 | 480 | -- void webaudio:setLooping(number loop) 481 | registerFunction("setLooping", "xwa:n", "n", function(self, args) 482 | local op1, op2 = args[2], args[3] 483 | ---@type WebAudio 484 | local this = op1[1](self, op1) 485 | local loop = op2[1](self, op2) 486 | 487 | checkPermissions(self) 488 | 489 | if this:IsValid() then 490 | return this:SetLooping(loop ~= 0) and 1 or 0 491 | else 492 | return self:throw("Invalid WebAudio instance!", 0) 493 | end 494 | end) 495 | 496 | -- number webaudio:setParent(entity parent) 497 | __e2setcost(5) 498 | registerFunction("setParent", "xwa:e", "n", function(self, args) 499 | local op1, op2 = args[2], args[3] 500 | ---@type WebAudio 501 | local this = op1[1](self, op1) 502 | local parent = op2[1](self, op2) 503 | 504 | if not IsValid(parent) then return self:throw("Parent is invalid!", 0) end 505 | checkPermissions(self) 506 | 507 | if this:IsValid() then 508 | return this:SetParent(parent) and 1 or 0 509 | else 510 | return self:throw("Invalid WebAudio instance!", 0) 511 | end 512 | end) 513 | 514 | -- number webaudio:setParent() 515 | registerFunction("setParent", "xwa:", "n", function(self, args) 516 | local op1 = args[2] 517 | ---@type WebAudio 518 | local this = op1[1](self, op1) 519 | 520 | checkPermissions(self) 521 | 522 | if this:IsValid() then 523 | return this:SetParent(nil) 524 | else 525 | return self:throw("Invalid WebAudio instance!", 0) 526 | end 527 | end) 528 | 529 | makeAlias("parentTo(xwa:e)", "setParent(xwa:e)") 530 | makeAlias("unparent(xwa:)", "setParent(xwa:)") 531 | 532 | -- number webaudio:set3DEnabled(number enabled) 533 | registerFunction("set3DEnabled", "xwa:n", "n", function(self, args) 534 | local op1, op2 = args[2], args[3] 535 | ---@type WebAudio 536 | local this = op1[1](self, op1) 537 | local enabled = op2[1](self, op2) 538 | 539 | checkPermissions(self) 540 | 541 | if this:IsValid() then 542 | return this:Set3DEnabled(enabled ~= 0) and 1 or 0 543 | else 544 | return self:throw("Invalid WebAudio instance!", 0) 545 | end 546 | end) 547 | 548 | --#endregion setters 549 | 550 | --#region ignore system 551 | 552 | --[[ 553 | NOTE: This ignore system is fine right now but if there's ever a way for the client to re-subscribe themselves it'll be a problem. 554 | 555 | For example WebAudio:Unsubscribe is called on wa_purge / wa_enable being set to 0. 556 | But what if wa_enable being set to 1 called Subscribe again in the future? 557 | ]] 558 | 559 | -- void webaudio:setIgnored(entity ply, number ignored) 560 | __e2setcost(25) 561 | registerFunction("setIgnored", "xwa:en", "n", function(self, args) 562 | local op1, op2, op3 = args[2], args[3], args[4] 563 | ---@type WebAudio 564 | local this = op1[1](self, op1) 565 | 566 | ---@type GPlayer 567 | local ply = op2[1](self, op2) 568 | 569 | ---@type number 570 | local ignored = op3[1](self, op3) 571 | 572 | checkPermissions(self) 573 | 574 | if not this:IsValid() then 575 | return self:throw("Invalid WebAudio instance!", 0) 576 | end 577 | 578 | if not IsValid(ply) then 579 | E2Lib.raiseException("Invalid player to ignore", nil, self.trace) 580 | end 581 | 582 | if not ply:IsPlayer() then 583 | E2Lib.raiseException("Expected Player, got Entity", nil, self.trace) 584 | end 585 | 586 | this.unsubbed_by_e2 = this.unsubbed_by_e2 or WireLib.RegisterPlayerTable() 587 | 588 | if ignored == 0 then 589 | -- Re-subscribe a user to the stream. (Only if they were unsubscribed by E2 previously.) 590 | if not this:IsSubscribed(ply) and this.unsubbed_by_e2[ply] then 591 | this:Subscribe(ply) 592 | this.unsubbed_by_e2[ply] = nil 593 | end 594 | else 595 | -- Ignore stream. Everyone can toggle this. 596 | if this:IsSubscribed(ply) then 597 | this.unsubbed_by_e2[ply] = true 598 | this:Unsubscribe(ply) 599 | end 600 | end 601 | end) 602 | 603 | -- void webaudio:setIgnored(array plys, number ignored) 604 | __e2setcost(5) 605 | registerFunction("setIgnored", "xwa:rn", "n", function(self, args) 606 | local op1, op2, op3 = args[2], args[3], args[4] 607 | ---@type WebAudio 608 | local this = op1[1](self, op1) 609 | 610 | ---@type table 611 | local plys = op2[1](self, op2) 612 | 613 | ---@type number 614 | local ignored = op3[1](self, op3) 615 | 616 | checkPermissions(self) 617 | 618 | if not this:IsValid() then 619 | return self:throw("Invalid WebAudio instance!", 0) 620 | end 621 | 622 | this.unsubbed_by_e2 = this.unsubbed_by_e2 or WireLib.RegisterPlayerTable() 623 | 624 | for _, ply in ipairs(plys) do 625 | self.prf = self.prf + 5 626 | if IsValid(ply) and ply:IsPlayer() then 627 | if ignored == 0 then 628 | -- Re-subscribe a user to the stream. (Only if they were unsubscribed by E2 previously.) 629 | if not this:IsSubscribed(ply) and this.unsubbed_by_e2[ply] then 630 | this:Subscribe(ply) 631 | this.unsubbed_by_e2[ply] = nil 632 | end 633 | else 634 | -- Ignore stream. Everyone can toggle this. 635 | if this:IsSubscribed(ply) then 636 | this.unsubbed_by_e2[ply] = true 637 | this:Unsubscribe(ply) 638 | end 639 | end 640 | end 641 | end 642 | end) 643 | 644 | registerFunction("getIgnored", "xwa:e", "n", function(self, args) 645 | local op1, op2 = args[2], args[3] 646 | ---@type WebAudio 647 | local this = op1[1](self, op1) 648 | ---@type GPlayer 649 | local ply = op2[1](self, op2) 650 | 651 | checkPermissions(self) 652 | 653 | if not this:IsValid() then 654 | return self:throw("Invalid WebAudio instance!", 0) 655 | end 656 | 657 | if not IsValid(ply) then 658 | return E2Lib.raiseException("Invalid player to check if ignored", nil, self.trace) 659 | end 660 | 661 | if not ply:IsPlayer() then 662 | return E2Lib.raiseException("Expected Player, got Entity", nil, self.trace) 663 | end 664 | 665 | return this:IsSubscribed(ply) and 0 or 1 666 | end) 667 | 668 | --#endregion ignore system 669 | 670 | 671 | --#region getters 672 | 673 | __e2setcost(2) 674 | -- number webaudio:isParented() 675 | registerFunction("isParented", "xwa:", "n", function(self, args) 676 | local op1 = args[2] 677 | ---@type WebAudio 678 | local this = op1[1](self, op1) 679 | 680 | if this:IsValid() then 681 | return this:IsParented() and 1 or 0 682 | else 683 | return self:throw("Invalid WebAudio instance!", 0) 684 | end 685 | end) 686 | 687 | -- vector webaudio:getPos() 688 | registerFunction("getPos", "xwa:", "v", function(self, args) 689 | local op1 = args[2] 690 | ---@type WebAudio 691 | local this = op1[1](self, op1) 692 | 693 | if this:IsValid() then 694 | return this:GetPos() or Vector(0, 0, 0) 695 | else 696 | return self:throw("Invalid WebAudio instance!", Vector(0, 0, 0)) 697 | end 698 | end) 699 | 700 | __e2setcost(4) 701 | -- number webaudio:getTime() 702 | registerFunction("getTime", "xwa:", "n", function(self, args) 703 | local op1 = args[2] 704 | ---@type WebAudio 705 | local this = op1[1](self, op1) 706 | 707 | if this:IsValid() then 708 | return this:GetTimeElapsed() 709 | else 710 | return self:throw("Invalid WebAudio instance!", -1) 711 | end 712 | end) 713 | 714 | -- number webaudio:getLength() 715 | registerFunction("getLength", "xwa:", "n", function(self, args) 716 | local op1 = args[2] 717 | ---@type WebAudio 718 | local this = op1[1](self, op1) 719 | 720 | if this:IsValid() then 721 | return this:GetLength() 722 | else 723 | return self:throw("Invalid WebAudio instance!", -1) 724 | end 725 | end) 726 | 727 | -- string webaudio:getFileName() 728 | registerFunction("getFileName", "xwa:", "s", function(self, args) 729 | local op1 = args[2] 730 | ---@type WebAudio 731 | local this = op1[1](self, op1) 732 | 733 | if this:IsValid() then 734 | return this:GetFileName() 735 | else 736 | return self:throw("Invalid WebAudio instance!", -1) 737 | end 738 | end) 739 | 740 | -- number webaudio:getFileName() 741 | registerFunction("getState", "xwa:", "n", function(self, args) 742 | local op1 = args[2] 743 | ---@type WebAudio 744 | local this = op1[1](self, op1) 745 | 746 | if this:IsValid() then 747 | return this:GetState() 748 | else 749 | return self:throw("Invalid WebAudio instance!", -1) 750 | end 751 | end) 752 | 753 | -- number webaudio:getVolume() 754 | registerFunction("getVolume", "xwa:", "n", function(self, args) 755 | local op1 = args[2] 756 | ---@type WebAudio 757 | local this = op1[1](self, op1) 758 | 759 | if this:IsValid() then 760 | return this:GetVolume() 761 | else 762 | return self:throw("Invalid WebAudio instance!", -1) 763 | end 764 | end) 765 | 766 | -- number webaudio:getRadius() 767 | registerFunction("getRadius", "xwa:", "n", function(self, args) 768 | local op1 = args[2] 769 | ---@type WebAudio 770 | local this = op1[1](self, op1) 771 | 772 | if this:IsValid() then 773 | return this:GetRadius() 774 | else 775 | return self:throw("Invalid WebAudio instance!", -1) 776 | end 777 | end) 778 | 779 | -- number webaudio:getLooping() 780 | registerFunction("getLooping", "xwa:", "n", function(self, args) 781 | local op1 = args[2] 782 | ---@type WebAudio 783 | local this = op1[1](self, op1) 784 | 785 | if this:IsValid() then 786 | return this:GetLooping() and 1 or 0 787 | else 788 | return self:throw("Invalid WebAudio instance!", 0) 789 | end 790 | end) 791 | 792 | -- number webaudio:get3DEnabled() 793 | registerFunction("get3DEnabled", "xwa:", "n", function(self, args) 794 | local op1 = args[2] 795 | ---@type WebAudio 796 | local this = op1[1](self, op1) 797 | 798 | if this:IsValid() then 799 | return this:Get3DEnabled() and 1 or 0 800 | else 801 | return self:throw("Invalid WebAudio instance!", 0) 802 | end 803 | end) 804 | 805 | -- array webaudio:getFFT() 806 | __e2setcost(800) 807 | registerFunction("getFFT", "xwa:", "r", function(self, args) 808 | local op1 = args[2] 809 | ---@type WebAudio 810 | local this = op1[1](self, op1) 811 | 812 | if this:IsValid() then 813 | if not FFTEnabled:GetBool() then 814 | return self:throw("FFT is disabled on this server!", {}) 815 | end 816 | 817 | return this:GetFFT(true, 0.08) or {} 818 | else 819 | return self:throw("Invalid WebAudio instance!", {}) 820 | end 821 | end) 822 | 823 | --#endregion getters 824 | 825 | --#region compat 826 | if SCCompat:GetBool() then 827 | ---@param index integer 828 | ---@param volume number? 829 | ---@param url string 830 | ---@param ent GEntity 831 | local function start(self, index, ent, url, volume) 832 | local existing = self.data.lazy_mfs[index] 833 | if existing then 834 | existing:Destroy(true) 835 | end 836 | 837 | local ply = self.player 838 | checkPermissions(self) 839 | 840 | if not WebAudio.isWhitelistedURL(url) then 841 | if WebAudio.Common.CustomWhitelist then 842 | E2Lib.raiseException("This URL is not whitelisted! The server has a custom whitelist.", nil, self.trace) 843 | else 844 | E2Lib.raiseException("This URL is not whitelisted! See github.com/Vurv78/WebAudio/blob/main/WHITELIST.md", nil, self.trace) 845 | end 846 | end 847 | 848 | -- Creation Time Quota 849 | if not CreationBurst:use(ply) then 850 | E2Lib.raiseException("You are creating WebAudios too fast. Check webAudioCanCreate before calling!", nil, self.trace) 851 | end 852 | 853 | -- Stream Count Quota 854 | if not checkCounter(ply) then 855 | E2Lib.raiseException("Reached maximum amount of WebAudio streams! Check webAudioCanCreate or webAudiosLeft before calling!", nil, self.trace) 856 | end 857 | 858 | local wa = registerStream(self, url, ply) 859 | wa:SetParent(ent) 860 | self.data.lazy_mfs[index] = wa 861 | 862 | timer.Simple(0, function() 863 | wa:Play() 864 | end) 865 | 866 | return 1 867 | end 868 | 869 | __e2setcost(500) 870 | 871 | registerFunction("streamHelp", "", "n", function(self, args) 872 | self.player:PrintMessage(HUD_PRINTTALK, "Streamcore -> WebAudio: https://github.com/Vurv78/WebAudio/wiki/From-StreamCore-To-WebAudio") 873 | end) 874 | 875 | registerFunction("streamCanStart", "", "n", function(self, args) 876 | local ply = self.player 877 | 878 | return ( 879 | CreationBurst:check(ply) 880 | and checkCounter(ply) 881 | ) and 1 or 0 882 | end) 883 | 884 | --! For you mfs who are too lazy to use the [Adaption Library](https://github.com/Vurv78/WebAudio/wiki/From-StreamCore-To-WebAudio#adaption-library) 885 | registerFunction("streamStart", "e:ns", "n", function(self, args) 886 | local op1, op2, op3 = args[2], args[3], args[4] 887 | local ent, idx, url = op1[1](self, op1), op2[1](self, op2), op3[1](self, op2) 888 | 889 | return start(self, idx, ent, url) 890 | end) 891 | 892 | registerFunction("streamStart", "e:nsn", "n", function(self, args) 893 | local op1, op2, op3, op4 = args[2], args[3], args[4], args[5] 894 | local ent, idx, url, volume = op1[1](self, op1), op2[1](self, op2), op3[1](self, op2), op4[1](self, op4) 895 | return start(self, idx, ent, url, volume) 896 | end) 897 | 898 | registerFunction("streamStart", "e:nns", "n", function(self, args) 899 | local op1, op2, op3, op4 = args[2], args[3], args[4], args[5] 900 | local ent, idx, volume, url = op1[1](self, op1), op2[1](self, op2), op3[1](self, op2), op4[1](self, op4) 901 | 902 | return start(self, idx, ent, url, volume) 903 | end) 904 | 905 | registerFunction("streamStop", "n", "n", function(self, args) 906 | local op1 = args[2] 907 | local idx = op1[1](self, op1) 908 | 909 | checkPermissions(self) 910 | 911 | local wa = self.data.lazy_mfs[idx] 912 | if wa then 913 | wa:Destroy(true) 914 | return 1 915 | else 916 | return self:throw("Invalid stream!", 0) 917 | end 918 | end) 919 | 920 | registerFunction("streamVolume", "nn", "n", function(self, args) 921 | local op1, op2 = args[2], args[3] 922 | local idx, volume = op1[1](self, op1), op2[1](self, op2) 923 | 924 | checkPermissions(self) 925 | 926 | local wa = self.data.lazy_mfs[idx] 927 | if wa then 928 | return wa:SetVolume( math.min(volume, MaxVolume:GetInt() / 100) ) 929 | and wa:Transmit(false) 930 | and 1 or 0 931 | else 932 | return self:throw("Invalid stream!", 0) 933 | end 934 | end) 935 | 936 | registerFunction("streamRadius", "nn", "n", function(self, args) 937 | local op1, op2 = args[2], args[3] 938 | local idx, radius = op1[1](self, op1), op2[1](self, op2) 939 | 940 | checkPermissions(self) 941 | 942 | local wa = self.data.lazy_mfs[idx] 943 | if wa then 944 | return wa:SetRadius( math.min(radius, MaxRadius:GetInt()) ) 945 | and wa:Transmit(false) 946 | and 1 or 0 947 | else 948 | return self:throw("Invalid stream!", 0) 949 | end 950 | end) 951 | 952 | -- How many seconds to wait in between streamStart calls: 953 | registerFunction("streamLimit", "", "n", function(self, args) 954 | return CREATE_REGEN 955 | end) 956 | 957 | registerFunction("streamMaxRadius", "", "n", function(self, args) 958 | return MaxRadius:GetInt() 959 | end) 960 | 961 | registerFunction("streamAdminOnly", "", "n", function(self, args) 962 | return AdminOnly 963 | end) 964 | 965 | registerFunction("streamDisable3D", "n", "n", function(self, args) 966 | local op1 = args[2] 967 | local idx = op1[1](self, op1) 968 | 969 | checkPermissions(self) 970 | 971 | local wa = self.data.lazy_mfs[idx] 972 | if wa then 973 | return wa:Set3DEnabled(false) 974 | and wa:Transmit(false) 975 | and 1 or 0 976 | else 977 | return self:throw("Invalid stream!", 0) 978 | end 979 | end) 980 | end 981 | --#endregion 982 | 983 | --#region cleanup 984 | 985 | registerCallback("construct", function(self) 986 | self.data.webaudio_streams = {} 987 | 988 | if SCCompat:GetBool() then 989 | self.data.lazy_mfs = {} 990 | end 991 | end) 992 | 993 | registerCallback("destruct", function(self) 994 | ---@type table 995 | local streams = self.data.webaudio_streams 996 | 997 | for k, stream in pairs(streams) do 998 | if stream:IsValid() then 999 | stream:Destroy(true) 1000 | end 1001 | streams[k] = nil 1002 | end 1003 | end) 1004 | 1005 | --#endregion cleanup 1006 | --------------------------------------------------------------------------------