├── .github └── workflows │ ├── build.yml │ ├── opus_1.4_x64.yml │ └── opus_lib_x64.yml ├── .gitignore ├── COPYING ├── README.md ├── SoundRemote.sln ├── SoundRemote ├── .gitignore ├── AudioCapture.cpp ├── AudioCapture.h ├── AudioResampler.cpp ├── AudioResampler.h ├── AudioUtil.cpp ├── AudioUtil.h ├── CapturePipe.cpp ├── CapturePipe.h ├── Clients.cpp ├── Clients.h ├── Controls.cpp ├── Controls.h ├── EncoderOpus.cpp ├── EncoderOpus.h ├── Keystroke.cpp ├── Keystroke.h ├── NetDefines.h ├── NetUtil.cpp ├── NetUtil.h ├── Server.cpp ├── Server.h ├── Settings.cpp ├── Settings.h ├── SettingsImpl.cpp ├── SettingsImpl.h ├── SoundRemote.ico ├── SoundRemote.rc ├── SoundRemote.vcxproj ├── SoundRemote.vcxproj.filters ├── SoundRemoteApp.cpp ├── SoundRemoteApp.h ├── UpdateChecker.cpp ├── UpdateChecker.h ├── Util.cpp ├── Util.h ├── framework.h ├── resource.h ├── resources │ ├── sound_off.ico │ └── sound_on.ico └── targetver.h ├── Tests ├── ClientsTest.cpp ├── EncoderOpusTest.cpp ├── KeystrokeTest.cpp ├── NetUtilTest.cpp ├── Tests.vcxproj ├── Tests.vcxproj.filters ├── UtilTest.cpp ├── header_tests │ ├── AudioCaptureHTest.cpp │ ├── AudioResamplerHTest.cpp │ ├── AudioUtilHTest.cpp │ ├── CapturePipeHTest.cpp │ ├── ClientsHTest.cpp │ ├── ControlsHTest.cpp │ ├── EncoderOpusHTest.cpp │ ├── KeystrokeHTest.cpp │ ├── NetDefinesHTest.cpp │ ├── NetUtilHTest.cpp │ ├── ServerHTest.cpp │ ├── SettingsHTest.cpp │ ├── SettingsImplHTest.cpp │ ├── SoundRemoteAppHTest.cpp │ ├── UpdateCheckerHTest.cpp │ └── UtilHTest.cpp ├── packages.config ├── pch.cpp └── pch.h ├── include └── opus │ ├── meson.build │ ├── opus.h │ ├── opus_custom.h │ ├── opus_defines.h │ ├── opus_multistream.h │ ├── opus_projection.h │ └── opus_types.h └── opus_license.txt /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & run tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | SOLUTION_FILE_PATH: . 11 | BINARY_FILE_PATH: bin 12 | BUILD_CONFIGURATION: Release 13 | PLATFORM: x64 14 | OPUS_VERSION: v1.5.2 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | build-opus: 21 | uses: ./.github/workflows/opus_lib_x64.yml 22 | with: 23 | opus-version: v1.5.2 24 | 25 | build: 26 | needs: build-opus 27 | runs-on: windows-latest 28 | 29 | steps: 30 | - name: Init environment files 31 | run: | 32 | echo "OPUS_CACHE_KEY=opus-lib-${{ env.OPUS_VERSION }}-x64" >> $env:GITHUB_ENV 33 | echo "OPUS_CACHE_PATH=opus-lib/${{ env.OPUS_VERSION }}/x64" >> $env:GITHUB_ENV 34 | 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | 38 | - name: Cache restore opus 39 | uses: actions/cache/restore@v4 40 | with: 41 | key: ${{ env.OPUS_CACHE_KEY }} 42 | path: ${{ env.OPUS_CACHE_PATH }} 43 | 44 | - name: Move opus lib file 45 | run: | 46 | mkdir -p lib/opus/${{env.PLATFORM}} 47 | mv ${{ env.OPUS_CACHE_PATH }}/opus.lib lib/opus/${{ env.PLATFORM }} 48 | 49 | - name: Install boost 50 | uses: MarkusJx/install-boost@v2.4.5 51 | id: install-boost 52 | with: 53 | boost_version: 1.83.0 54 | platform_version: 2022 55 | toolset: msvc 56 | 57 | - name: Setup MSBuild 58 | uses: microsoft/setup-msbuild@v2 59 | 60 | - name: Restore NuGet packages 61 | working-directory: ${{ env.GITHUB_WORKSPACE }} 62 | run: nuget restore ${{ env.SOLUTION_FILE_PATH }} 63 | 64 | - name: Build 65 | working-directory: ${{ env.GITHUB_WORKSPACE }} 66 | env: 67 | BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }}\include 68 | run: | 69 | msbuild ${{ env.SOLUTION_FILE_PATH }} -m -p:Configuration=${{ env.BUILD_CONFIGURATION }} -p:Platform=${{ env.PLATFORM }} -p:OutDir="${{ github.workspace }}\${{ env.BINARY_FILE_PATH }}\" 70 | 71 | - name: Run tests 72 | run: | 73 | & "${{ env.BINARY_FILE_PATH }}\Tests.exe" 74 | 75 | - name: Upload artifact 76 | id: upload_artifact 77 | uses: actions/upload-artifact@v4 78 | with: 79 | name: release-x64 80 | path: ${{ env.BINARY_FILE_PATH }}\SoundRemote.exe 81 | compression-level: 9 82 | 83 | - name: Create "Artifact SHA-256" notice 84 | run: echo "::notice title=Artifact SHA-256::${{ steps.upload_artifact.outputs.artifact-digest }}" 85 | -------------------------------------------------------------------------------- /.github/workflows/opus_1.4_x64.yml: -------------------------------------------------------------------------------- 1 | name: Build and cache opus 1.4 x64 2 | 3 | on: 4 | workflow_call: 5 | workflow_dispatch: 6 | 7 | env: 8 | OPUS_CACHE_PATH: opus-lib/1.4/x64 9 | OPUS_CACHE_KEY: opus-lib-1.4-x64 10 | 11 | jobs: 12 | check-cache: 13 | runs-on: windows-2022 14 | outputs: 15 | cache-hit: ${{ steps.cache-check-opus.outputs.cache-hit }} 16 | steps: 17 | - name: Check cache 18 | id: cache-check-opus 19 | uses: actions/cache/restore@v4 20 | with: 21 | key: ${{env.OPUS_CACHE_KEY}} 22 | path: ${{env.OPUS_CACHE_PATH}} 23 | lookup-only: true 24 | 25 | build: 26 | needs: check-cache 27 | if: needs.check-cache.outputs.cache-hit != 'true' 28 | runs-on: windows-2022 29 | env: 30 | OPUS_SOLUTION_FILE_PATH: opus-1.4/win32/VS2015 31 | BUILD_CONFIGURATION: Release 32 | PLATFORM: x64 33 | steps: 34 | - name: Download and unpack src 35 | run: | 36 | $OpusUri = 'https://github.com/xiph/opus/archive/refs/tags/v1.4.zip' 37 | $OpusZip = 'opus1.4.zip' 38 | Start-BitsTransfer -Dynamic -Source $OpusUri -Destination $OpusZip 39 | Expand-Archive -Path $OpusZip -DestinationPath . 40 | 41 | - name: Setup MSBuild 42 | uses: microsoft/setup-msbuild@v2 43 | 44 | - name: Build 45 | run: msbuild ${{env.OPUS_SOLUTION_FILE_PATH}}/opus.vcxproj -m -p:PlatformToolset=v143 -p:Configuration=${{env.BUILD_CONFIGURATION}} -p:Platform=${{env.PLATFORM}} -p:OutDir="${{github.workspace}}/${{env.OPUS_CACHE_PATH}}/" 46 | 47 | - name: Cache 48 | uses: actions/cache/save@v4 49 | with: 50 | key: ${{env.OPUS_CACHE_KEY}} 51 | path: ${{env.OPUS_CACHE_PATH}} 52 | -------------------------------------------------------------------------------- /.github/workflows/opus_lib_x64.yml: -------------------------------------------------------------------------------- 1 | name: Build and cache opus lib x64 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | opus-version: 7 | required: true 8 | type: string 9 | 10 | jobs: 11 | check-cache: 12 | runs-on: windows-2022 13 | outputs: 14 | cache-hit: ${{ steps.cache-check-opus.outputs.cache-hit }} 15 | steps: 16 | - name: Check cache 17 | id: cache-check-opus 18 | uses: actions/cache/restore@v4 19 | with: 20 | key: opus-lib-${{ inputs.opus-version }}-x64 21 | path: opus-lib/${{ inputs.opus-version }}/x64 22 | lookup-only: true 23 | 24 | build: 25 | needs: check-cache 26 | if: needs.check-cache.outputs.cache-hit != 'true' 27 | runs-on: windows-2022 28 | env: 29 | OPUS_SOLUTION_FILE_PATH: cmake 30 | BUILD_CONFIGURATION: MinSizeRel 31 | steps: 32 | - name: Init environment files 33 | run: | 34 | echo "CACHE_KEY=opus-lib-${{ inputs.opus-version }}-x64" >> $env:GITHUB_ENV 35 | echo "CACHE_PATH=opus-lib/${{ inputs.opus-version }}/x64" >> $env:GITHUB_ENV 36 | 37 | - name: Checkout opus 38 | uses: actions/checkout@v4 39 | with: 40 | repository: xiph/opus 41 | ref: ${{ inputs.opus-version }} 42 | 43 | - name: Generate solution 44 | run: | 45 | cd cmake 46 | cmake .. -G "Visual Studio 17 2022" -A x64 47 | 48 | - name: Setup MSBuild 49 | uses: microsoft/setup-msbuild@v2 50 | 51 | - name: Build 52 | run: msbuild ${{ env.OPUS_SOLUTION_FILE_PATH }}/opus.vcxproj -m -p:PlatformToolset=v143 -p:Configuration=${{ env.BUILD_CONFIGURATION }} -p:Platform=x64 -p:OutDir="${{ github.workspace }}/${{ env.CACHE_PATH }}/" 53 | 54 | - name: Cache 55 | uses: actions/cache/save@v4 56 | with: 57 | key: ${{ env.CACHE_KEY }} 58 | path: ${{ env.CACHE_PATH }} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # Project-specific files 7 | [Ss]ettings.ini 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml 402 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SoundRemote server 2 | Desktop app that pairs up with Android device via [SoundRemote client](https://github.com/SoundRemote/client-android) to: 3 | * Capture and send audio to the client device. 4 | * Emulate keyboard shortcuts received from the client. Certain shortcuts like `Ctrl + Alt + Delete` or `Win + L` aren't currently supported. 5 | 6 | ![Main window](https://github.com/ashipo/SoundRemote-server/assets/24320267/be5120e6-f8fd-4232-9869-5d7c96d2d50e "Main window") 7 | 8 | ## Build 9 | Prerequisites: 10 | - Microsoft Visual Studio 2022. 11 | - [Opus audio codec](https://github.com/xiph/opus) library. 12 | Build `opus.lib` and put it in `$(SolutionDir)\lib\opus\x64` or `$(SolutionDir)\lib\opus\Win32`. 13 | - [Boost](https://www.boost.org/) has to be accessible by `BOOST_ROOT` environment variable. 14 | It must point to the Boost root directory, for example `C:\Program Files\boost\boost_1_82_0`. 15 | 16 | ## Testing 17 | Tests are implemented with GoogleTest. To run tests install the [gmock](https://www.nuget.org/packages/gmock/) NuGet package from Google. 18 | -------------------------------------------------------------------------------- /SoundRemote.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.34202.233 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SoundRemote", "SoundRemote\SoundRemote.vcxproj", "{D85222EF-3A69-4496-8F74-1E7E652CECFF}" 7 | EndProject 8 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Tests", "Tests\Tests.vcxproj", "{2AB4BAFF-28F1-4DA0-91A8-4E5125D59F99}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|x64 = Debug|x64 13 | Debug|x86 = Debug|x86 14 | Release|x64 = Release|x64 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {D85222EF-3A69-4496-8F74-1E7E652CECFF}.Debug|x64.ActiveCfg = Debug|x64 19 | {D85222EF-3A69-4496-8F74-1E7E652CECFF}.Debug|x64.Build.0 = Debug|x64 20 | {D85222EF-3A69-4496-8F74-1E7E652CECFF}.Debug|x86.ActiveCfg = Debug|Win32 21 | {D85222EF-3A69-4496-8F74-1E7E652CECFF}.Debug|x86.Build.0 = Debug|Win32 22 | {D85222EF-3A69-4496-8F74-1E7E652CECFF}.Release|x64.ActiveCfg = Release|x64 23 | {D85222EF-3A69-4496-8F74-1E7E652CECFF}.Release|x64.Build.0 = Release|x64 24 | {D85222EF-3A69-4496-8F74-1E7E652CECFF}.Release|x86.ActiveCfg = Release|Win32 25 | {D85222EF-3A69-4496-8F74-1E7E652CECFF}.Release|x86.Build.0 = Release|Win32 26 | {2AB4BAFF-28F1-4DA0-91A8-4E5125D59F99}.Debug|x64.ActiveCfg = Debug|x64 27 | {2AB4BAFF-28F1-4DA0-91A8-4E5125D59F99}.Debug|x64.Build.0 = Debug|x64 28 | {2AB4BAFF-28F1-4DA0-91A8-4E5125D59F99}.Debug|x86.ActiveCfg = Debug|Win32 29 | {2AB4BAFF-28F1-4DA0-91A8-4E5125D59F99}.Debug|x86.Build.0 = Debug|Win32 30 | {2AB4BAFF-28F1-4DA0-91A8-4E5125D59F99}.Release|x64.ActiveCfg = Release|x64 31 | {2AB4BAFF-28F1-4DA0-91A8-4E5125D59F99}.Release|x64.Build.0 = Release|x64 32 | {2AB4BAFF-28F1-4DA0-91A8-4E5125D59F99}.Release|x86.ActiveCfg = Release|Win32 33 | {2AB4BAFF-28F1-4DA0-91A8-4E5125D59F99}.Release|x86.Build.0 = Release|Win32 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(ExtensibilityGlobals) = postSolution 39 | SolutionGuid = {E65E4CA0-726D-4536-8B00-EA5E7A4CDBD6} 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /SoundRemote/.gitignore: -------------------------------------------------------------------------------- 1 | # Build results 2 | x64/ 3 | 4 | # User specific files 5 | *.user 6 | 7 | # Visual C++ cache 8 | *.aps 9 | -------------------------------------------------------------------------------- /SoundRemote/AudioCapture.cpp: -------------------------------------------------------------------------------- 1 | #include "AudioCapture.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include "Util.h" 11 | 12 | using namespace boost::asio; 13 | using namespace std::chrono_literals; 14 | using namespace Audio; 15 | 16 | template 17 | struct AwaitableTimer { 18 | AwaitableTimer(io_context& io, Duration duration) : timer_(io, std::chrono::steady_clock::now()), duration_(duration) {} 19 | bool await_ready() const { return false; } 20 | void await_suspend(std::coroutine_handle<> h) { 21 | timer_.expires_at(timer_.expiry() + duration_); 22 | std::shared_ptr destroyed = destroyed_; 23 | timer_.async_wait([h, destroyed](boost::system::error_code ec) mutable { 24 | if (ec) { 25 | if (ec != boost::asio::error::operation_aborted) { 26 | throw std::runtime_error(Util::makeAppErrorText("Timer1", ec.what())); 27 | } 28 | } else { 29 | if (!*destroyed) { 30 | h.resume(); 31 | } 32 | } 33 | }); 34 | } 35 | void await_resume() const noexcept {} 36 | ~AwaitableTimer() { 37 | *destroyed_ = true; 38 | } 39 | private: 40 | steady_timer timer_; 41 | Duration duration_; 42 | std::shared_ptr destroyed_ = std::make_shared(false); 43 | }; 44 | 45 | //Anon namespace for helpers 46 | namespace { 47 | /// 48 | /// Allocates and returns a pointer to a WaveFormat initiated from the passed Audio::Format. 49 | /// 50 | /// Audio format to initiate from. 51 | /// Pointer to the created WaveFormat. 52 | WAVEFORMATEXTENSIBLE* createWaveFormat(const Audio::Format& format) { 53 | auto result = reinterpret_cast(CoTaskMemAlloc(sizeof(WAVEFORMATEXTENSIBLE))); 54 | if (NULL == result) { 55 | return NULL; 56 | } 57 | result->Format.nChannels = format.channelCount; 58 | result->Format.nSamplesPerSec = format.sampleRate; 59 | result->Format.wBitsPerSample = format.sampleSize; 60 | result->Format.cbSize = 22; 61 | result->Format.nBlockAlign = result->Format.nChannels * result->Format.wBitsPerSample / 8; 62 | result->Format.nAvgBytesPerSec = result->Format.nSamplesPerSec * result->Format.nBlockAlign; 63 | result->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; 64 | result->Samples.wValidBitsPerSample = result->Format.wBitsPerSample; 65 | switch (format.channelCount) { 66 | case 1: 67 | result->dwChannelMask = KSAUDIO_SPEAKER_MONO; 68 | break; 69 | case 2: 70 | result->dwChannelMask = KSAUDIO_SPEAKER_STEREO; 71 | break; 72 | default: 73 | result->dwChannelMask = 0; 74 | } 75 | switch (format.sampleType) { 76 | case Audio::SampleType::SignedInt: 77 | case Audio::SampleType::UnSignedInt: 78 | result->SubFormat = KSDATAFORMAT_SUBTYPE_PCM; 79 | break; 80 | case Audio::SampleType::Float: 81 | result->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; 82 | break; 83 | case Audio::SampleType::Unknown: 84 | result->SubFormat = KSDATAFORMAT_SUBTYPE_NONE; 85 | break; 86 | } 87 | return result; 88 | } 89 | 90 | class BufferReleaser { 91 | public: 92 | BufferReleaser(IAudioCaptureClient* client, UINT32 nFrames) : 93 | client_(client), nFrames_(nFrames) { 94 | assert(client != nullptr); 95 | } 96 | HRESULT release() { 97 | released_ = true; 98 | return client_->ReleaseBuffer(nFrames_); 99 | } 100 | ~BufferReleaser() { 101 | if (released_) return; 102 | HRESULT hr = release(); 103 | // If destructor was called and buffer isn't released yet means capture coroutine is being destroyed. 104 | if (FAILED(hr)) { 105 | Util::showError(Audio::audioErrorText(hr, Audio::Location::CAPTURE_ACC_RELEASEBUFFER)); 106 | } 107 | } 108 | BufferReleaser(const BufferReleaser&) = delete; 109 | private: 110 | IAudioCaptureClient* client_ = nullptr; 111 | UINT32 nFrames_ = 0; 112 | bool released_ = false; 113 | }; 114 | }; 115 | 116 | //------AudioCapture------> 117 | 118 | AudioCapture::AudioCapture(const std::wstring& deviceId, Audio::Format requestedFormat, boost::asio::io_context& ioContext) : ioContext_(ioContext) { 119 | //throw Audio::Error("AudioCapture::ctor"); 120 | 121 | constexpr int REFTIMES_PER_SEC = 10'000'000; 122 | constexpr int REFTIMES_PER_MILLISEC = 10'000; 123 | 124 | HRESULT hr; 125 | hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); 126 | throwOnError(hr, Audio::Location::CAPTURE_COINITIALIZE); 127 | coUninitializer_ = std::make_unique(); 128 | 129 | CComPtr enumerator; 130 | hr = enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator)); 131 | throwOnError(hr, Audio::Location::CAPTURE_COCREATEINSTANCE); 132 | 133 | CComPtr device; 134 | hr = enumerator->GetDevice(deviceId.c_str(), &device); 135 | throwOnError(hr, Audio::Location::CAPTURE_GETDEVICE); 136 | 137 | hr = device->Activate(__uuidof(IAudioMeterInformation), CLSCTX_ALL, nullptr, reinterpret_cast(&meterInfo_)); 138 | throwOnError(hr, Audio::Location::CAPTURE_ACTIVATE_METERINFO); 139 | 140 | hr = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast(&audioClient_)); 141 | throwOnError(hr, Audio::Location::CAPTURE_ACTIVATE_AUDIOCLIENT); 142 | 143 | CComPtr endpoint; 144 | hr = device->QueryInterface(IID_PPV_ARGS(&endpoint)); 145 | throwOnError(hr, Audio::Location::CAPTURE_QUERY_ENDPOINT); 146 | 147 | EDataFlow deviceFlow; 148 | hr = endpoint->GetDataFlow(&deviceFlow); 149 | throwOnError(hr, Audio::Location::CAPTURE_ENDPOINT_GETDATAFLOW); 150 | 151 | // Figure out capture wave format 152 | requestedWaveFormat_ = WaveFormat(createWaveFormat(requestedFormat)); 153 | if (nullptr == requestedWaveFormat_) { 154 | throwOnError(E_OUTOFMEMORY, Audio::Location::CAPTURE_MEMALLOC); 155 | } 156 | WAVEFORMATEXTENSIBLE* pSupportedFormat = nullptr; 157 | hr = audioClient_->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, 158 | reinterpret_cast(requestedWaveFormat_.get()), 159 | reinterpret_cast(&pSupportedFormat)); 160 | supportedWaveFormat_.reset(pSupportedFormat); 161 | pSupportedFormat = nullptr; 162 | 163 | switch (hr) { 164 | case S_OK: //requested format is supported, supportedWaveFormat_ is NULL 165 | resampleRequired_ = false; 166 | supportedWaveFormat_.reset(reinterpret_cast(CoTaskMemAlloc(sizeof(WAVEFORMATEXTENSIBLE)))); 167 | if (nullptr == supportedWaveFormat_) { 168 | throwOnError(E_OUTOFMEMORY, Audio::Location::CAPTURE_MEMALLOC); 169 | } 170 | *supportedWaveFormat_ = *requestedWaveFormat_; 171 | break; 172 | case S_FALSE: //requested format is not supported, supportedWaveFormat_ was initialized 173 | resampleRequired_ = true; 174 | break; 175 | case AUDCLNT_E_UNSUPPORTED_FORMAT: //requested format is not supported, supportedWaveFormat_ is NULL 176 | resampleRequired_ = true; 177 | hr = audioClient_->GetMixFormat(reinterpret_cast(&pSupportedFormat)); 178 | throwOnError(hr, Audio::Location::CAPTURE_AC_GETMIXFORMAT); 179 | supportedWaveFormat_.reset(pSupportedFormat); 180 | pSupportedFormat = nullptr; 181 | break; 182 | default: 183 | throwOnError(hr, Audio::Location::CAPTURE_AC_ISFORMATSUPPORTED); 184 | break; 185 | } 186 | 187 | // Initialize AudioClient 188 | DWORD streamFlags = (eRender == deviceFlow) ? AUDCLNT_STREAMFLAGS_LOOPBACK : 0; 189 | //REFERENCE_TIME hnsRequestedDuration = REFTIMES_PER_SEC; 190 | REFERENCE_TIME hnsRequestedDuration = 0; // Requested buffer duration 191 | hr = audioClient_->Initialize( 192 | AUDCLNT_SHAREMODE_SHARED, 193 | streamFlags, 194 | hnsRequestedDuration, 195 | 0, 196 | reinterpret_cast(supportedWaveFormat_.get()), 197 | nullptr); 198 | throwOnError(hr, deviceFlow == eCapture ? 199 | Audio::Location::CAPTURE_AC_INITIALIZE_CAPTURE : 200 | Audio::Location::CAPTURE_AC_INITIALIZE_RENDER); 201 | 202 | // Get the size of the allocated buffer. 203 | UINT32 bufferFrameCount; 204 | hr = audioClient_->GetBufferSize(&bufferFrameCount); 205 | throwOnError(hr, Audio::Location::CAPTURE_AC_GETBUFFERSIZE); 206 | 207 | hr = audioClient_->GetService(IID_PPV_ARGS(&captureClient_)); 208 | throwOnError(hr, Audio::Location::CAPTURE_AC_GETSERVICE); 209 | 210 | // Calculate the actual duration of the allocated buffer. 211 | REFERENCE_TIME hnsActualDuration = static_cast(static_cast(REFTIMES_PER_SEC) * 212 | bufferFrameCount / supportedWaveFormat_->Format.nSamplesPerSec); 213 | bufferDuration_ = BufferDuration(hnsActualDuration); 214 | } 215 | 216 | AudioCapture::~AudioCapture() { 217 | } 218 | 219 | CaptureCoroutine AudioCapture::capture() { 220 | //throw Audio::Error("AudioCapture::capture start"); 221 | HRESULT hr; 222 | 223 | hr = audioClient_->Start(); 224 | Audio::throwOnError(hr, Audio::Location::CAPTURE_AC_START); 225 | std::unique_ptr audioClientStop(audioClient_, [](IAudioClient* client_) { 226 | HRESULT hr = client_->Stop(); 227 | if (FAILED(hr)) // Shouldn't throw from a dtor, so just show the error. 228 | Util::showError(Audio::audioErrorText(hr, Audio::Location::CAPTURE_AC_STOP)); 229 | }); 230 | 231 | const auto timerPeriod = bufferDuration_ / 2; 232 | auto uncompensatedSilenceDuration = BufferDuration::zero(); 233 | unsigned int silenceCompensationFrames = 0; 234 | const std::chrono::duration periodSeconds = timerPeriod; 235 | const unsigned int silenceBufferSize = std::lround(supportedWaveFormat_->Format.nSamplesPerSec * periodSeconds.count()) * 236 | supportedWaveFormat_->Format.nBlockAlign; 237 | std::vector silenceBuffer( silenceBufferSize ); 238 | AwaitableTimer timer(ioContext_, timerPeriod); 239 | for (;;) { 240 | co_await timer; 241 | 242 | UINT32 packetLength = 0; 243 | hr = captureClient_->GetNextPacketSize(&packetLength); 244 | Audio::throwOnError(hr, Audio::Location::CAPTURE_ACC_GETNEXTPACKETSIZE); 245 | 246 | // Silence compensation 247 | if (packetLength == 0) { 248 | uncompensatedSilenceDuration += timerPeriod; 249 | if (uncompensatedSilenceDuration >= bufferDuration_) { 250 | co_yield{ reinterpret_cast(silenceBuffer.data()), silenceBufferSize }; 251 | uncompensatedSilenceDuration -= timerPeriod; 252 | } 253 | } else { 254 | uncompensatedSilenceDuration = BufferDuration::zero(); 255 | } 256 | 257 | while (packetLength != 0) { 258 | BYTE* pData; 259 | UINT32 numFramesAvailable; 260 | DWORD flags; 261 | hr = captureClient_->GetBuffer( 262 | &pData, 263 | &numFramesAvailable, 264 | &flags, nullptr, nullptr); 265 | Audio::throwOnError(hr, Audio::Location::CAPTURE_ACC_GETBUFFER); 266 | BufferReleaser bufferReleaser (captureClient_, numFramesAvailable); 267 | 268 | //if (flags & AUDCLNT_BUFFERFLAGS_SILENT) {} 269 | const auto size = numFramesAvailable * supportedWaveFormat_->Format.nBlockAlign; 270 | co_yield{ reinterpret_cast(pData), size }; 271 | //throw Audio::Error("AudioCapture::capture loop"); 272 | 273 | hr = bufferReleaser.release(); 274 | Audio::throwOnError(hr, Audio::Location::CAPTURE_ACC_RELEASEBUFFER); 275 | 276 | hr = captureClient_->GetNextPacketSize(&packetLength); 277 | Audio::throwOnError(hr, Audio::Location::CAPTURE_ACC_GETNEXTPACKETSIZE); 278 | } 279 | } 280 | } 281 | 282 | bool AudioCapture::resampleRequired() const { 283 | return resampleRequired_; 284 | } 285 | 286 | WAVEFORMATEXTENSIBLE* AudioCapture::requestedWaveFormat() const { 287 | return requestedWaveFormat_.get(); 288 | } 289 | 290 | WAVEFORMATEXTENSIBLE* AudioCapture::capturedWaveFormat() const { 291 | return supportedWaveFormat_.get(); 292 | } 293 | 294 | float AudioCapture::getPeakValue() const { 295 | float result; 296 | const HRESULT hr = meterInfo_->GetPeakValue(&result); 297 | if (hr != S_OK) { 298 | return -1; 299 | } 300 | return result; 301 | } 302 | -------------------------------------------------------------------------------- /SoundRemote/AudioCapture.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include "AudioUtil.h" 15 | 16 | struct IAudioCaptureClient; 17 | struct IAudioClient; 18 | struct IAudioMeterInformation; 19 | 20 | struct CaptureCoroutine; 21 | 22 | class AudioCapture { 23 | public: 24 | /// 25 | /// Creates AudioCapture. 26 | /// 27 | /// Device id string. 28 | /// Requested audio format. 29 | /// boost::asio::io_context to use by an internal timer. 30 | AudioCapture(const std::wstring& deviceId, Audio::Format requestedFormat, boost::asio::io_context& ioContext); 31 | ~AudioCapture(); 32 | 33 | /// 34 | /// Awaitable capture coroutine. 35 | /// 36 | CaptureCoroutine capture(); 37 | 38 | /// 39 | /// Is resampling of the captured audio required, i.e. requested audio format is not supported by the audio device. 40 | /// 41 | /// True if resampling required, false otherwise. 42 | bool resampleRequired() const; 43 | 44 | /// 45 | /// Gets the requested wave format represented as a WAVEFORMATEXTENSIBLE structure. 46 | /// 47 | /// A pointer to a WAVEFORMATEXTENSIBLE structure. 48 | /// Pointer remains valid until the AudioCapture object exists. 49 | WAVEFORMATEXTENSIBLE* requestedWaveFormat() const; 50 | 51 | /// 52 | /// Gets wave format that is actually supported by the audio device. 53 | /// Wave format represented as a WAVEFORMATEXTENSIBLE structure. 54 | /// 55 | /// A pointer to a WAVEFORMATEXTENSIBLE structure. 56 | /// Pointer remains valid until the AudioCapture object exists. 57 | WAVEFORMATEXTENSIBLE* capturedWaveFormat() const; 58 | 59 | /// 60 | /// Gets the peak sample value for the captured audio. 61 | /// 62 | /// Peak value as a number in range from 0.0 to 1.0. Returns -1 on fail. 63 | float getPeakValue() const; 64 | private: 65 | using WaveFormat = std::unique_ptr>; 66 | using BufferDuration = std::chrono::duration>; //hundreds nanoseconds 67 | 68 | boost::asio::io_context& ioContext_; 69 | bool resampleRequired_ = false; 70 | BufferDuration bufferDuration_ = BufferDuration::zero(); 71 | std::unique_ptr coUninitializer_; 72 | 73 | WaveFormat requestedWaveFormat_; 74 | WaveFormat supportedWaveFormat_; 75 | CComPtr audioClient_; 76 | CComPtr captureClient_; 77 | CComPtr meterInfo_; 78 | }; 79 | 80 | struct [[nodiscard]] CaptureCoroutine { 81 | struct promise_type; 82 | using Handle = std::coroutine_handle; 83 | 84 | struct promise_type { 85 | std::span pcmAudio; 86 | std::coroutine_handle<> awaiting_coroutine_; 87 | //std::exception_ptr exception_; 88 | 89 | CaptureCoroutine get_return_object() { 90 | return { Handle::from_promise(*this) }; 91 | } 92 | std::suspend_never initial_suspend() { return {}; } 93 | std::suspend_never final_suspend() noexcept { return {}; } 94 | void unhandled_exception() { 95 | auto exception = std::current_exception(); 96 | std::rethrow_exception(exception); 97 | } 98 | void return_void() noexcept {} 99 | auto yield_value(const std::span& data) { 100 | pcmAudio = data; 101 | 102 | struct transfer_awaitable { 103 | std::coroutine_handle<> awaiting_coroutine; 104 | 105 | bool await_ready() noexcept { return false; } 106 | std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) noexcept { 107 | return awaiting_coroutine ? awaiting_coroutine : std::noop_coroutine(); 108 | } 109 | void await_resume() noexcept {} 110 | }; 111 | return transfer_awaitable{ awaiting_coroutine_ }; 112 | } 113 | }; 114 | 115 | Handle h_; 116 | CaptureCoroutine(Handle h) :h_{ h } {} 117 | ~CaptureCoroutine() { h_.destroy(); } 118 | operator Handle() const { return h_; } 119 | 120 | //explicit operator bool() { 121 | // return !h_.done(); 122 | //} 123 | 124 | template 125 | struct AudioAwaiter { 126 | Handle captureCoro; 127 | bool await_ready() { 128 | return captureCoro.promise().pcmAudio.size() > 0; 129 | } 130 | void await_suspend(std::coroutine_handle awaiting) { 131 | captureCoro.promise().awaiting_coroutine_ = awaiting; 132 | } 133 | auto await_resume() { 134 | auto result = captureCoro.promise().pcmAudio; 135 | captureCoro.promise().pcmAudio = {}; 136 | return result; 137 | } 138 | }; 139 | 140 | auto operator co_await() { 141 | return AudioAwaiter{ h_ }; 142 | } 143 | }; 144 | -------------------------------------------------------------------------------- /SoundRemote/AudioResampler.cpp: -------------------------------------------------------------------------------- 1 | #include "AudioResampler.h" 2 | 3 | #include // MUST INCLUDE BEFORE 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "AudioUtil.h" 10 | 11 | AudioResampler::AudioResampler(_In_ const WAVEFORMATEXTENSIBLE* inputFormat, _In_ const WAVEFORMATEXTENSIBLE* outputFormat, 12 | _In_ boost::asio::streambuf& outBuffer): outBuffer_(outBuffer) { 13 | HRESULT hr; 14 | CComPtr transformUnk; 15 | hr = transformUnk.CoCreateInstance(__uuidof(CResamplerMediaObject)); 16 | throwOnError(hr, Audio::Location::RESAMPLER_COCREATEINSTANCE); 17 | // Get resampler MFT 18 | hr = transformUnk->QueryInterface(IID_PPV_ARGS(&transform_)); 19 | throwOnError(hr, Audio::Location::RESAMPLER_QUERY_TRANSFORM); 20 | // Set the best conversion quality (1-60) 21 | CComPtr resamplerProps; 22 | hr = transformUnk->QueryInterface(IID_PPV_ARGS(&resamplerProps)); 23 | throwOnError(hr, Audio::Location::RESAMPLER_QUERY_PROPS); 24 | hr = resamplerProps->SetHalfFilterLength(60); 25 | throwOnError(hr, Audio::Location::RESAMPLER_PROPS_SETHALFFILTERLENGTH); 26 | // Create and set input media type 27 | CComPtr inMediaType; 28 | hr = MFCreateMediaType(&inMediaType); 29 | throwOnError(hr, Audio::Location::RESAMPLER_CREATEMEDIATYPE_INPUT); 30 | hr = MFInitMediaTypeFromWaveFormatEx(inMediaType, reinterpret_cast(inputFormat), 31 | sizeof(WAVEFORMATEXTENSIBLE)); 32 | throwOnError(hr, Audio::Location::RESAMPLER_INITMEDIATYPE_INPUT); 33 | hr = transform_->SetInputType(0, inMediaType, 0); 34 | throwOnError(hr, Audio::Location::RESAMPLER_TRANSFORM_SETINPUTTYPE); 35 | // Create and set output media type 36 | CComPtr outMediaType; 37 | hr = MFCreateMediaType(&outMediaType); 38 | throwOnError(hr, Audio::Location::RESAMPLER_CREATEMEDIATYPE_OUTPUT); 39 | hr = MFInitMediaTypeFromWaveFormatEx(outMediaType, reinterpret_cast(outputFormat), 40 | sizeof(WAVEFORMATEXTENSIBLE)); 41 | throwOnError(hr, Audio::Location::RESAMPLER_INITMEDIATYPE_OUTPUT); 42 | transform_->SetOutputType(0, outMediaType, 0); 43 | throwOnError(hr, Audio::Location::RESAMPLER_TRANSFORM_SETOUTPUTTYPE); 44 | // Start transform stream 45 | hr = transform_->ProcessMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0); 46 | throwOnError(hr, Audio::Location::RESAMPLER_TRANSFORM_MESSAGE_BEGIN); 47 | hr = transform_->ProcessMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0); 48 | throwOnError(hr, Audio::Location::RESAMPLER_TRANSFORM_MESSAGE_START); 49 | outBufferSizeMultiplier_ = 2 * static_cast(outputFormat->Format.nAvgBytesPerSec) / 50 | static_cast(inputFormat->Format.nAvgBytesPerSec); 51 | } 52 | 53 | AudioResampler::~AudioResampler() { 54 | if (transform_) { 55 | transform_->ProcessMessage(MFT_MESSAGE_NOTIFY_END_STREAMING, NULL); 56 | transform_->ProcessMessage(MFT_MESSAGE_NOTIFY_END_OF_STREAM, NULL); 57 | } 58 | } 59 | 60 | void AudioResampler::resample(_In_ const std::span& pcmAudio) { 61 | HRESULT hr; 62 | // Create MediaBuffer with incoming data 63 | CComPtr inSampleBuffer; 64 | hr = MFCreateMemoryBuffer(static_cast(pcmAudio.size_bytes()), &inSampleBuffer); 65 | throwOnError(hr, Audio::Location::RESAMPLER_CREATE_INPUT_BUFFER); 66 | BYTE* inBytes = nullptr; 67 | hr = inSampleBuffer->Lock(&inBytes, nullptr, nullptr); 68 | throwOnError(hr, Audio::Location::RESAMPLER_INPUT_BUFFER_LOCK); 69 | memcpy(inBytes, pcmAudio.data(), pcmAudio.size_bytes()); 70 | hr = inSampleBuffer->Unlock(); 71 | throwOnError(hr, Audio::Location::RESAMPLER_INPUT_BUFFER_UNLOCK); 72 | inBytes = nullptr; 73 | hr = inSampleBuffer->SetCurrentLength(static_cast(pcmAudio.size_bytes())); 74 | throwOnError(hr, Audio::Location::RESAMPLER_INPUT_BUFFER_SETLENGTH); 75 | // Create Sample from that MediaBuffer 76 | CComPtr inSample; 77 | hr = MFCreateSample(&inSample); 78 | throwOnError(hr, Audio::Location::RESAMPLER_CREATE_INPUT_SAMPLE); 79 | hr = inSample->AddBuffer(inSampleBuffer); 80 | throwOnError(hr, Audio::Location::RESAMPLER_INPUT_SAMPLE_ADDBUFFER); 81 | // Process the Sample 82 | hr = transform_->ProcessInput(0, inSample, 0); 83 | throwOnError(hr, Audio::Location::RESAMPLER_TRANSFORM_PROCESSINPUT); 84 | // Create output buffer 85 | MFT_OUTPUT_STREAM_INFO outStreamInfo; 86 | hr = transform_->GetOutputStreamInfo(0, &outStreamInfo); 87 | throwOnError(hr, Audio::Location::RESAMPLER_TRANSFORM_GETOUTPUTSTREAMINFO); 88 | MFT_OUTPUT_DATA_BUFFER outputDataBuffer { 0, nullptr, 0, nullptr }; 89 | CComPtr outSampleBuffer; 90 | CComPtr outSample; 91 | // If the MFT does not set one of this flags, the client must allocate the samples for the output stream. 92 | const bool outSampleProvided = 0 != (outStreamInfo.dwFlags & (MFT_OUTPUT_STREAM_PROVIDES_SAMPLES | 93 | MFT_OUTPUT_STREAM_CAN_PROVIDE_SAMPLES)); 94 | if (!outSampleProvided) { 95 | const DWORD outSampleBufferSize = static_cast(outBufferSizeMultiplier_ * pcmAudio.size_bytes()); 96 | hr = MFCreateMemoryBuffer(outSampleBufferSize, &outSampleBuffer); 97 | throwOnError(hr, Audio::Location::RESAMPLER_CREATE_OUTPUT_BUFFER); 98 | hr = MFCreateSample(&outSample); 99 | throwOnError(hr, Audio::Location::RESAMPLER_CREATE_OUTPUT_SAMPLE); 100 | hr = outSample->AddBuffer(outSampleBuffer); 101 | throwOnError(hr, Audio::Location::RESAMPLER_OUTPUT_SAMPLE_ADDBUFFER); 102 | outputDataBuffer.pSample = outSample; 103 | } 104 | DWORD totalBytesResampled = 0; 105 | for (;;) { 106 | DWORD dwStatus = 0; 107 | hr = transform_->ProcessOutput(0, 1, &outputDataBuffer, &dwStatus); 108 | // If the sample was allocated by the MFT, attach it to a CComPtr so it gets released automatically. 109 | if (outSampleProvided) { 110 | outSample.Attach(outputDataBuffer.pSample); 111 | } 112 | // Release the events that the MFT might allocate. 113 | SAFE_RELEASE(outputDataBuffer.pEvents); 114 | if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) { 115 | break; 116 | } 117 | throwOnError(hr, Audio::Location::RESAMPLER_TRANSFORM_PROCESSOUTPUT); 118 | CComPtr processedDataBuffer; 119 | // The caller must release the interface. 120 | hr = outSample->ConvertToContiguousBuffer(&processedDataBuffer); 121 | throwOnError(hr, Audio::Location::RESAMPLER_OUTPUT_SAMPLE_CONVERT); 122 | DWORD cbBytes = 0; 123 | hr = processedDataBuffer->GetCurrentLength(&cbBytes); 124 | throwOnError(hr, Audio::Location::RESAMPLER_PROCESSED_BUFFER_GETCURRENTLENGTH); 125 | BYTE* outBytes = nullptr; 126 | hr = processedDataBuffer->Lock(&outBytes, nullptr, nullptr); 127 | throwOnError(hr, Audio::Location::RESAMPLER_PROCESSED_BUFFER_LOCK); 128 | outBuffer_.sputn(reinterpret_cast(outBytes), cbBytes); 129 | totalBytesResampled += cbBytes; 130 | hr = processedDataBuffer->Unlock(); 131 | throwOnError(hr, Audio::Location::RESAMPLER_PROCESSED_BUFFER_UNLOCK); 132 | }; 133 | } 134 | -------------------------------------------------------------------------------- /SoundRemote/AudioResampler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | 11 | struct IMFTransform; 12 | 13 | class AudioResampler { 14 | public: 15 | AudioResampler(_In_ const WAVEFORMATEXTENSIBLE* inputFormat, _In_ const WAVEFORMATEXTENSIBLE* outputFormat, 16 | _In_ boost::asio::streambuf& outBuffer); 17 | ~AudioResampler(); 18 | void resample(_In_ const std::span& pcmAudio); 19 | private: 20 | CComPtr transform_; 21 | boost::asio::streambuf& outBuffer_; 22 | double outBufferSizeMultiplier_ = 0.0; 23 | }; 24 | -------------------------------------------------------------------------------- /SoundRemote/AudioUtil.cpp: -------------------------------------------------------------------------------- 1 | #include "AudioUtil.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | std::unordered_map Audio::getEndpointDevices(const EDataFlow dataFlow) { 9 | HRESULT hr; 10 | 11 | hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); 12 | exitOnError(hr, Location::UTIL_GETDEVICES_COINITIALIZE); 13 | CoUninitializer coUninitializer; 14 | 15 | CComPtr enumerator; 16 | hr = enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator)); 17 | exitOnError(hr, Location::UTIL_GETDEVICES_CREATE_ENUMERATOR); 18 | 19 | CComPtr devices; 20 | hr = enumerator->EnumAudioEndpoints(dataFlow, DEVICE_STATE_ACTIVE, &devices); 21 | exitOnError(hr, Location::UTIL_GETDEVICES_ENUMERATOR_ENUMENDPOINTS); 22 | 23 | UINT deviceCount; 24 | hr = devices->GetCount(&deviceCount); 25 | exitOnError(hr, Location::UTIL_GETDEVICES_ENDPOINTS_GETCOUNT); 26 | 27 | std::unordered_map result; 28 | for (UINT i = 0; i < deviceCount; ++i) { 29 | CComPtr device; 30 | hr = devices->Item(i, &device); 31 | exitOnError(hr, Location::UTIL_GETDEVICES_DEVICES_ITEM); 32 | 33 | LPWSTR pDeviceId = nullptr; 34 | hr = device->GetId(&pDeviceId); 35 | exitOnError(hr, Location::UTIL_GETDEVICES_DEVICE_GETID); 36 | std::unique_ptr deviceId(pDeviceId, CoTaskMemFree); 37 | pDeviceId = nullptr; 38 | 39 | CComPtr props; 40 | hr = device->OpenPropertyStore(STGM_READ, &props); 41 | exitOnError(hr, Location::UTIL_GETDEVICES_DEVICE_OPENPROPERTYSTORE); 42 | 43 | PROPVARIANT varName; 44 | PropVariantInit(&varName); 45 | hr = props->GetValue(PKEY_Device_FriendlyName, &varName); 46 | exitOnError(hr, Location::UTIL_GETDEVICES_PROPS_GETVALUE); 47 | 48 | result[varName.pwszVal] = deviceId.get(); 49 | } 50 | return result; 51 | } 52 | 53 | std::wstring Audio::getDefaultDevice(EDataFlow flow) { 54 | HRESULT hr; 55 | 56 | hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); 57 | exitOnError(hr, Location::UTIL_GETDEFAULTDEVICE_COINITIALIZE); 58 | CoUninitializer coUninitializer; 59 | 60 | CComPtr enumerator; 61 | hr = enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator)); 62 | exitOnError(hr, Location::UTIL_GETDEFAULTDEVICE_CREATE_ENUMERATOR); 63 | 64 | CComPtr device; 65 | hr = enumerator->GetDefaultAudioEndpoint(flow, eConsole, &device); 66 | exitOnError(hr, Location::UTIL_GETDEFAULTDEVICE_ENUMERATOR_GETDEFAULTENDPOINT); 67 | 68 | LPWSTR deviceId = nullptr; 69 | hr = device->GetId(&deviceId); 70 | exitOnError(hr, Location::UTIL_GETDEFAULTDEVICE_DEVICE_GETID); 71 | 72 | std::wstring result{ deviceId }; 73 | CoTaskMemFree(deviceId); 74 | return result; 75 | } 76 | 77 | void Audio::throwOnError(const HRESULT hr, Location where) { 78 | if (FAILED(hr)) 79 | throw Audio::Error(audioErrorText(hr, where)); 80 | } 81 | 82 | void Audio::exitOnError(const HRESULT hr, Location where) { 83 | if (FAILED(hr)) { 84 | Util::showError(audioErrorText(hr, where)); 85 | exit(EXIT_FAILURE); 86 | } 87 | } 88 | 89 | void Audio::processError(const long errorCode, Location where) { 90 | throw Audio::Error(audioErrorText(errorCode, where)); 91 | } 92 | 93 | std::string Audio::audioErrorText(const HRESULT errorCode, Location where) { 94 | // Special cases 95 | if (Location::CAPTURE_AC_INITIALIZE_CAPTURE == where && E_ACCESSDENIED == errorCode) { 96 | return "Microphone access denied. You can change this in the system privacy settings."; 97 | } 98 | std::ostringstream ss; 99 | ss << "Audio capture error " << static_cast(where) << ". [" << std::hex << std::showbase << errorCode << ']'; 100 | return ss.str(); 101 | } 102 | -------------------------------------------------------------------------------- /SoundRemote/AudioUtil.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "Util.h" 10 | 11 | #define EXIT_ON_ERROR(hres) \ 12 | if (FAILED(hres)) { goto Exit; } 13 | 14 | #define THROW_ON_ERROR(hres, location) \ 15 | if (FAILED(hres)) { \ 16 | hr = hres; \ 17 | where = location; \ 18 | goto Exit; } 19 | 20 | #define SAFE_RELEASE(punk) \ 21 | if ((punk) != nullptr) \ 22 | { (punk)->Release(); (punk) = nullptr; } 23 | 24 | namespace Audio { 25 | // Constants, types, enums 26 | 27 | constexpr auto defaultRenderDeviceId = -1; 28 | constexpr auto defaultCaptureDeviceId = -2; 29 | 30 | enum class Compression { none = 0, kbps_64 = 64'000, kbps_128 = 128'000, kbps_192 = 192'000, kbps_256 = 256'000, kbps_320 = 320'000 }; 31 | 32 | namespace Opus { 33 | // Supported sample rates 34 | enum class SampleRate { khz_8 = 8'000, khz_12 = 12'000, khz_16 = 16'000, khz_24 = 24'000, khz_48 = 48'000 }; 35 | // Supported channels 36 | enum class Channels { mono = 1, stereo = 2 }; 37 | // Opus frame length in ms. Opus can encode frames of 2.5, 5, 10, 20, 40, or 60 ms. This value determines Opus frame size. 38 | // At 48 kHz the permitted values of Opus frame size are 120(2.5ms), 240(5ms), 480(10ms), 960(20ms), 1920(40ms), and 2880(60ms). 39 | constexpr int frameLength = 10; 40 | // Maximum Opus packet size in bytes. 41 | constexpr int maxPacketSize = 2 * static_cast(Compression::kbps_320) * frameLength / (1000 * 8); 42 | } 43 | 44 | enum class SampleType { 45 | Unknown = 0, 46 | SignedInt = 1, 47 | UnSignedInt = 2, 48 | Float = 3 49 | }; 50 | 51 | enum class Location { 52 | NOWHERE = 0, 53 | CAPTURE_COINITIALIZE = 1, 54 | CAPTURE_COCREATEINSTANCE = 2, 55 | CAPTURE_GETDEVICE = 3, 56 | CAPTURE_ACTIVATE_AUDIOCLIENT = 4, 57 | CAPTURE_QUERY_ENDPOINT = 5, 58 | CAPTURE_ENDPOINT_GETDATAFLOW = 6, 59 | CAPTURE_AC_ISFORMATSUPPORTED = 7, 60 | CAPTURE_AC_GETMIXFORMAT = 8, 61 | CAPTURE_AC_INITIALIZE_RENDER = 9, 62 | CAPTURE_AC_INITIALIZE_CAPTURE = 10, 63 | CAPTURE_MEMALLOC = 11, 64 | CAPTURE_AC_GETBUFFERSIZE = 12, 65 | CAPTURE_AC_GETSERVICE = 13, 66 | CAPTURE_AC_START = 20, 67 | CAPTURE_AC_STOP = 21, 68 | CAPTURE_ACC_GETNEXTPACKETSIZE = 22, 69 | CAPTURE_ACC_GETBUFFER = 23, 70 | CAPTURE_ACC_RELEASEBUFFER = 24, 71 | CAPTURE_ACTIVATE_METERINFO = 25, 72 | 73 | RESAMPLER_COCREATEINSTANCE = 101, 74 | RESAMPLER_QUERY_TRANSFORM = 102, 75 | RESAMPLER_QUERY_PROPS = 103, 76 | RESAMPLER_PROPS_SETHALFFILTERLENGTH = 104, 77 | RESAMPLER_CREATEMEDIATYPE_INPUT = 105, 78 | RESAMPLER_INITMEDIATYPE_INPUT = 106, 79 | RESAMPLER_TRANSFORM_SETINPUTTYPE = 107, 80 | RESAMPLER_CREATEMEDIATYPE_OUTPUT = 108, 81 | RESAMPLER_INITMEDIATYPE_OUTPUT = 109, 82 | RESAMPLER_TRANSFORM_SETOUTPUTTYPE = 110, 83 | RESAMPLER_TRANSFORM_MESSAGE_FLUSH = 111, 84 | RESAMPLER_TRANSFORM_MESSAGE_BEGIN = 112, 85 | RESAMPLER_TRANSFORM_MESSAGE_START = 113, 86 | RESAMPLER_CREATE_INPUT_BUFFER = 114, 87 | RESAMPLER_INPUT_BUFFER_LOCK = 115, 88 | RESAMPLER_INPUT_BUFFER_UNLOCK = 116, 89 | RESAMPLER_INPUT_BUFFER_SETLENGTH = 117, 90 | RESAMPLER_CREATE_INPUT_SAMPLE = 118, 91 | RESAMPLER_INPUT_SAMPLE_ADDBUFFER = 119, 92 | RESAMPLER_TRANSFORM_PROCESSINPUT = 120, 93 | RESAMPLER_TRANSFORM_GETOUTPUTSTREAMINFO = 121, 94 | RESAMPLER_CREATE_OUTPUT_BUFFER = 122, 95 | RESAMPLER_CREATE_OUTPUT_SAMPLE = 123, 96 | RESAMPLER_OUTPUT_SAMPLE_ADDBUFFER = 124, 97 | RESAMPLER_TRANSFORM_PROCESSOUTPUT = 125, 98 | RESAMPLER_OUTPUT_SAMPLE_CONVERT = 126, 99 | RESAMPLER_PROCESSED_BUFFER_GETCURRENTLENGTH = 127, 100 | RESAMPLER_PROCESSED_BUFFER_LOCK = 128, 101 | RESAMPLER_PROCESSED_BUFFER_UNLOCK = 129, 102 | 103 | ENCODER_CREATE = 202, 104 | ENCODER_SET_BITRATE= 203, 105 | ENCODER_ENCODE = 204, 106 | 107 | UTIL_GETDEVICES_COINITIALIZE = 301, 108 | UTIL_GETDEVICES_CREATE_ENUMERATOR = 302, 109 | UTIL_GETDEVICES_ENUMERATOR_ENUMENDPOINTS = 303, 110 | UTIL_GETDEVICES_ENDPOINTS_GETCOUNT = 304, 111 | UTIL_GETDEVICES_DEVICES_ITEM = 305, 112 | UTIL_GETDEVICES_DEVICE_GETID = 306, 113 | UTIL_GETDEVICES_DEVICE_OPENPROPERTYSTORE = 307, 114 | UTIL_GETDEVICES_PROPS_GETVALUE = 308, 115 | UTIL_GETDEFAULTDEVICE_COINITIALIZE = 310, 116 | UTIL_GETDEFAULTDEVICE_CREATE_ENUMERATOR = 311, 117 | UTIL_GETDEFAULTDEVICE_ENUMERATOR_GETDEFAULTENDPOINT = 312, 118 | UTIL_GETDEFAULTDEVICE_DEVICE_GETID = 313 119 | }; 120 | 121 | // Data classes and structs 122 | 123 | struct Format { 124 | int sampleRate = 48000; 125 | int channelCount = 2; 126 | int sampleSize = 16; 127 | SampleType sampleType = SampleType::SignedInt; 128 | Util::Endian byteOrder = Util::Endian::Little; 129 | }; 130 | 131 | // Utility classes and structs 132 | 133 | class Error : public std::runtime_error { 134 | public: 135 | Error(const std::string& what) : std::runtime_error(what) {}; 136 | }; 137 | 138 | // Function object to be used as a deleter with std::unique_ptr to the objects requiring CoTaskMemFree. 139 | // std::unique_ptr> waveFormat; 140 | template 141 | struct CoDeleter { 142 | void operator()(T* var) const { CoTaskMemFree(var); } 143 | }; 144 | 145 | /// 146 | /// Calls CoUninitialize() in dtor. 147 | /// 148 | struct CoUninitializer { 149 | ~CoUninitializer() { CoUninitialize(); }; 150 | }; 151 | 152 | // Functions 153 | 154 | std::unordered_map getEndpointDevices(const EDataFlow dataFlow); 155 | 156 | /// 157 | /// Gets default device id string. 158 | /// 159 | /// EDataFlow::eCapture or EDataFlow::eRender. 160 | /// Device id string. 161 | std::wstring getDefaultDevice(EDataFlow flow); 162 | 163 | void throwOnError(const HRESULT hr, Location where); 164 | /// 165 | /// Shows error message and terminates the app if passed HRESULT is an error. 166 | /// 167 | /// operation result to check. 168 | /// describes error location. 169 | void exitOnError(const HRESULT hr, Location where); 170 | void processError(const long errorCode, Location where); 171 | std::string audioErrorText(const HRESULT errorCode, Location where); 172 | } 173 | -------------------------------------------------------------------------------- /SoundRemote/CapturePipe.cpp: -------------------------------------------------------------------------------- 1 | #include "CapturePipe.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "AudioCapture.h" 7 | #include "AudioResampler.h" 8 | #include "AudioUtil.h" 9 | #include "Clients.h" 10 | #include "EncoderOpus.h" 11 | #include "Server.h" 12 | #include "Util.h" 13 | 14 | struct [[nodiscard]] PipeCoroutine { 15 | struct promise_type; 16 | using Handle = std::coroutine_handle; 17 | 18 | struct promise_type { 19 | PipeCoroutine get_return_object() { 20 | return Handle::from_promise(*this); 21 | } 22 | std::suspend_never initial_suspend() { return {}; } 23 | std::suspend_never final_suspend() noexcept { return {}; } 24 | void return_void() {} 25 | void unhandled_exception() { 26 | auto exception = std::current_exception(); 27 | std::rethrow_exception(exception); 28 | } 29 | }; 30 | 31 | Handle h_; 32 | PipeCoroutine(Handle h) :h_{ h } {} 33 | ~PipeCoroutine() { /*h_.destroy();*/ } 34 | operator Handle() const { return h_; } 35 | //PipeCoroutine(const PipeCoroutine&) = delete; 36 | }; 37 | 38 | Net::Packet::SequenceNumberType CapturePipe::audioSequenceNumber_ = 1u; 39 | 40 | CapturePipe::CapturePipe(const std::wstring& deviceId, std::shared_ptr server, boost::asio::io_context& ioContext, bool muted): 41 | device_(deviceId), server_(server), io_context_(ioContext), muted_(muted) { 42 | //throw std::runtime_error("CapturePipe::ctr"); 43 | Audio::Format requestedFormat; 44 | audioCapture_ = std::make_unique(deviceId, requestedFormat, ioContext); 45 | if (audioCapture_->resampleRequired()) { 46 | auto capturedWaveFormat = audioCapture_->capturedWaveFormat(); 47 | auto requestedWaveFormat = audioCapture_->requestedWaveFormat(); 48 | audioResampler_ = std::make_unique(capturedWaveFormat, requestedWaveFormat, pcmAudioBuffer_); 49 | } 50 | opusInputSize_ = EncoderOpus::getInputSize(EncoderOpus::getFrameSize(Audio::Opus::SampleRate::khz_48), Audio::Opus::Channels::stereo); 51 | } 52 | 53 | CapturePipe::~CapturePipe() { 54 | stop(); 55 | } 56 | 57 | void CapturePipe::start() { 58 | pipeCoro_ = std::make_unique(process()); 59 | } 60 | 61 | float CapturePipe::getPeakValue() const { 62 | return audioCapture_->getPeakValue(); 63 | } 64 | 65 | void CapturePipe::setMuted(bool muted) { 66 | muted_ = muted; 67 | } 68 | 69 | void CapturePipe::onClientsUpdate(std::forward_list clients) { 70 | std::unordered_set newCompressions; 71 | std::unordered_set existingCompressions; 72 | for (auto&& client : clients) { 73 | if (encoders_.contains(client.compression)) { 74 | existingCompressions.insert(client.compression); 75 | } else { 76 | newCompressions.insert(client.compression); 77 | } 78 | } 79 | 80 | if (newCompressions.empty() && existingCompressions.size() == encoders_.size()) { 81 | return; 82 | } 83 | 84 | std::erase_if(encoders_, [&](const auto& item) { 85 | return !existingCompressions.contains(item.first); 86 | }); 87 | for (auto&& it: newCompressions) { 88 | if (it == Audio::Compression::none) { 89 | encoders_[it] = std::unique_ptr(); 90 | } else { 91 | encoders_[it] = std::make_unique(it, Audio::Opus::SampleRate::khz_48, 92 | Audio::Opus::Channels::stereo); 93 | } 94 | } 95 | } 96 | 97 | PipeCoroutine CapturePipe::process() { 98 | //throw std::runtime_error("CapturePipe::process start"); 99 | auto audioCapture = audioCapture_->capture(); 100 | for (;;) { 101 | auto capturedAudio = co_await audioCapture; 102 | if (!muted_) { 103 | auto server = server_.lock(); 104 | if (server && haveClients()) { 105 | process(capturedAudio, server); 106 | } 107 | } 108 | //throw std::runtime_error("CapturePipe::process loop"); 109 | audioCapture.h_(); 110 | } 111 | } 112 | 113 | void CapturePipe::stop() { 114 | if (pipeCoro_) { 115 | pipeCoro_->h_.destroy(); 116 | pipeCoro_.reset(); 117 | } 118 | } 119 | 120 | bool CapturePipe::haveClients() const { 121 | return !encoders_.empty(); 122 | } 123 | 124 | void CapturePipe::process(std::span pcmAudio, std::shared_ptr server) { 125 | if (audioCapture_->resampleRequired()) { 126 | audioResampler_->resample(pcmAudio); 127 | } else { 128 | pcmAudioBuffer_.sputn(pcmAudio.data(), pcmAudio.size()); 129 | } 130 | while (pcmAudioBuffer_.data().size() >= opusInputSize_) { 131 | for (auto&& [compression, encoder] : encoders_) { 132 | if (compression == Audio::Compression::none) { 133 | std::vector uncompressed(opusInputSize_); 134 | std::copy_n( 135 | reinterpret_cast(pcmAudioBuffer_.data().data()), 136 | opusInputSize_, 137 | uncompressed.data() 138 | ); 139 | server->sendAudio(compression, audioSequenceNumber_, uncompressed); 140 | } else { 141 | std::vector encodedPacket(Audio::Opus::maxPacketSize); 142 | const auto packetSize = encoder->encode( 143 | static_cast(pcmAudioBuffer_.data().data()), 144 | encodedPacket.data() 145 | ); 146 | encodedPacket.resize(packetSize); 147 | if (packetSize > 0) { 148 | server->sendAudio(compression, audioSequenceNumber_, encodedPacket); 149 | } 150 | } 151 | } 152 | ++audioSequenceNumber_; 153 | pcmAudioBuffer_.consume(opusInputSize_); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /SoundRemote/CapturePipe.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "AudioUtil.h" 14 | #include "NetDefines.h" 15 | 16 | class AudioCapture; 17 | class AudioResampler; 18 | class EncoderOpus; 19 | class Server; 20 | struct PipeCoroutine; 21 | struct ClientInfo; 22 | 23 | class CapturePipe { 24 | public: 25 | CapturePipe(const std::wstring& deviceId, std::shared_ptr server, boost::asio::io_context& io_context, 26 | bool muted = false); 27 | ~CapturePipe(); 28 | void start(); 29 | float getPeakValue() const; 30 | void setMuted(bool muted); 31 | void onClientsUpdate(std::forward_list clients); 32 | private: 33 | // Capturing coroutine 34 | PipeCoroutine process(); 35 | // Destroys the capturing coroutine 36 | void stop(); 37 | void process(std::span pcmAudio, std::shared_ptr server); 38 | bool haveClients() const; 39 | 40 | boost::asio::io_context& io_context_; 41 | std::unique_ptr pipeCoro_; 42 | std::unique_ptr audioCapture_; 43 | std::unique_ptr audioResampler_; 44 | std::weak_ptr server_; 45 | const std::wstring device_; 46 | boost::asio::streambuf pcmAudioBuffer_; 47 | std::atomic_bool muted_ = false; 48 | std::unordered_map> encoders_; 49 | int opusInputSize_; 50 | static Net::Packet::SequenceNumberType audioSequenceNumber_; 51 | }; 52 | -------------------------------------------------------------------------------- /SoundRemote/Clients.cpp: -------------------------------------------------------------------------------- 1 | #include "Clients.h" 2 | 3 | Clients::Clients(int timeoutSeconds) : timeoutSeconds_(timeoutSeconds) {} 4 | 5 | void Clients::add(const Net::Address& address, Audio::Compression compression) { 6 | const std::unique_lock lock(clientsMutex_); 7 | if (clients_.contains(address)) { 8 | clients_[address]->updateLastContact(); 9 | if (clients_[address]->compression() == compression) { 10 | return; 11 | } 12 | clients_[address]->setCompression(compression); 13 | updateAndNotify(); 14 | } else { 15 | clients_[address] = std::make_unique(compression); 16 | updateAndNotify(); 17 | } 18 | } 19 | 20 | void Clients::setCompression(const Net::Address& address, Audio::Compression compression) { 21 | const std::unique_lock lock(clientsMutex_); 22 | if (!clients_.contains(address) || clients_[address]->compression() == compression) { 23 | return; 24 | } 25 | clients_[address]->setCompression(compression); 26 | updateAndNotify(); 27 | } 28 | 29 | void Clients::keep(const Net::Address& address) { 30 | const std::unique_lock lock(clientsMutex_); 31 | if (!clients_.contains(address)) { 32 | return; 33 | } 34 | clients_[address]->updateLastContact(); 35 | } 36 | 37 | void Clients::remove(const Net::Address& address) { 38 | const std::unique_lock lock(clientsMutex_); 39 | if (clients_.erase(address)) { 40 | updateAndNotify(); 41 | } 42 | } 43 | 44 | void Clients::addClientsListener(ClientsUpdateCallback listener) { 45 | clientsListeners_.push_front(listener); 46 | listener(clientInfos_); 47 | } 48 | 49 | size_t Clients::removeClientsListener(ClientsUpdateCallback listener) { 50 | return clientsListeners_.remove_if([&](ClientsUpdateCallback f) { 51 | return f.target_type().name() == listener.target_type().name(); 52 | }); 53 | } 54 | 55 | void Clients::maintain() { 56 | const std::unique_lock lock(clientsMutex_); 57 | bool clientRemoved = false; 58 | const auto now = std::chrono::steady_clock::now(); 59 | for (auto&& it = clients_.begin(); it != clients_.end();) { 60 | std::chrono::duration elapsedSeconds = now - it->second->lastContact(); 61 | if (elapsedSeconds.count() > timeoutSeconds_) { 62 | it = clients_.erase(it); 63 | clientRemoved = true; 64 | } else { 65 | ++it; 66 | } 67 | } 68 | if (clientRemoved) { 69 | updateAndNotify(); 70 | } 71 | } 72 | 73 | void Clients::updateInfos() { 74 | clientInfos_.clear(); 75 | for (auto&& client : clients_) { 76 | clientInfos_.push_front({ client.first, client.second->compression() }); 77 | } 78 | } 79 | 80 | void Clients::notifyListeners() const { 81 | for (auto&& listener : clientsListeners_) { 82 | listener(clientInfos_); 83 | } 84 | } 85 | 86 | void Clients::updateAndNotify() { 87 | updateInfos(); 88 | notifyListeners(); 89 | } 90 | 91 | // Client 92 | 93 | Clients::Client::Client(Audio::Compression compression): compression_(compression) { 94 | updateLastContact(); 95 | } 96 | 97 | void Clients::Client::updateLastContact() { 98 | lastContact_ = std::chrono::steady_clock::now(); 99 | } 100 | 101 | void Clients::Client::setCompression(Audio::Compression compression) { 102 | compression_ = compression; 103 | } 104 | 105 | Clients::TimePoint Clients::Client::lastContact() const { 106 | return lastContact_; 107 | } 108 | 109 | Audio::Compression Clients::Client::compression() const { 110 | return compression_; 111 | } 112 | 113 | bool operator==(const ClientInfo& lhs, const ClientInfo& rhs) { 114 | return lhs.address == rhs.address && 115 | lhs.compression == rhs.compression; 116 | } -------------------------------------------------------------------------------- /SoundRemote/Clients.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "AudioUtil.h" 11 | #include "NetDefines.h" 12 | 13 | struct ClientInfo; 14 | 15 | class Clients { 16 | using TimePoint = std::chrono::steady_clock::time_point; 17 | using ClientsUpdateCallback = std::function)>; 18 | public: 19 | Clients(int timeoutSeconds = 5); 20 | void add(const Net::Address& address, Audio::Compression compression); 21 | void setCompression(const Net::Address& address, Audio::Compression compression); 22 | void keep(const Net::Address& address); 23 | void remove(const Net::Address& address); 24 | void addClientsListener(ClientsUpdateCallback listener); 25 | size_t removeClientsListener(ClientsUpdateCallback listener); 26 | void maintain(); 27 | 28 | private: 29 | class Client { 30 | public: 31 | Client(Audio::Compression compression); 32 | void updateLastContact(); 33 | void setCompression(Audio::Compression compression); 34 | TimePoint lastContact() const; 35 | Audio::Compression compression() const; 36 | private: 37 | Audio::Compression compression_ = Audio::Compression::none; 38 | TimePoint lastContact_{}; 39 | }; 40 | 41 | void updateInfos(); 42 | void notifyListeners() const; 43 | void updateAndNotify(); 44 | 45 | const int timeoutSeconds_; 46 | std::unordered_map> clients_; 47 | std::forward_list clientInfos_; 48 | std::shared_mutex clientsMutex_; 49 | // listeners are not synchronized, the list is modified on program start and on device change 50 | std::forward_list clientsListeners_; 51 | }; 52 | 53 | struct ClientInfo { 54 | Net::Address address; 55 | Audio::Compression compression; 56 | ClientInfo(Net::Address addr, Audio::Compression br) : address(addr), compression(br) {} 57 | friend bool operator==(const ClientInfo& lhs, const ClientInfo& rhs); 58 | }; 59 | -------------------------------------------------------------------------------- /SoundRemote/Controls.cpp: -------------------------------------------------------------------------------- 1 | #include "Controls.h" 2 | 3 | #include 4 | 5 | #include "resource.h" 6 | 7 | MuteButton::MuteButton(HWND hParent, const Rect& rect, const std::wstring& name) { 8 | HINSTANCE hInst = GetModuleHandle(nullptr); 9 | button_ = CreateWindowW(WC_BUTTON, name.c_str(), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_ICON | BS_AUTOCHECKBOX | BS_PUSHLIKE, 10 | rect.x, rect.y, rect.w, rect.h, hParent, NULL, hInst, NULL); 11 | iconSoundOn_ = LoadImage(hInst, MAKEINTRESOURCE(IDI_SOUND_ON), IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR); 12 | iconSoundOff_ = LoadImage(hInst, MAKEINTRESOURCE(IDI_SOUND_OFF), IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR); 13 | updateIcon(false); 14 | } 15 | 16 | void MuteButton::onClick() { 17 | bool muted = isMuted(); 18 | updateIcon(muted); 19 | if (stateCallback_) { 20 | stateCallback_(muted); 21 | } 22 | } 23 | 24 | HWND MuteButton::handle() const { 25 | return button_; 26 | } 27 | 28 | bool MuteButton::isMuted() const { 29 | return SendMessage(button_, BM_GETCHECK, 0, 0) == BST_CHECKED; 30 | } 31 | 32 | void MuteButton::setStateCallback(std::function callback) { 33 | stateCallback_ = callback; 34 | } 35 | 36 | void MuteButton::updateIcon(bool muted) { 37 | HANDLE iconHandle; 38 | if (muted) { 39 | iconHandle = iconSoundOff_; 40 | } else { 41 | iconHandle = iconSoundOn_; 42 | } 43 | SendMessage(button_, BM_SETIMAGE, IMAGE_ICON, (LPARAM)iconHandle); 44 | } 45 | -------------------------------------------------------------------------------- /SoundRemote/Controls.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | struct Rect; 9 | 10 | class MuteButton { 11 | public: 12 | MuteButton(HWND hParent, const Rect& rect, const std::wstring& name); 13 | void onClick(); 14 | HWND handle() const; 15 | void setStateCallback(std::function callback); 16 | private: 17 | HWND button_ = nullptr; 18 | HANDLE iconSoundOn_ = nullptr; 19 | HANDLE iconSoundOff_ = nullptr; 20 | std::function stateCallback_ = nullptr; 21 | 22 | bool isMuted() const; 23 | void updateIcon(bool muted); 24 | }; 25 | 26 | struct Rect { 27 | int x = 0; 28 | int y = 0; 29 | int w = 0; 30 | int h = 0; 31 | 32 | Rect() {} 33 | Rect(int _x, int _y, int _w, int _h) : x(_x), y(_y), w(_w), h(_h) {} 34 | }; 35 | -------------------------------------------------------------------------------- /SoundRemote/EncoderOpus.cpp: -------------------------------------------------------------------------------- 1 | #include "EncoderOpus.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "Util.h" 9 | 10 | EncoderOpus::EncoderOpus(Audio::Compression compression, Audio::Opus::SampleRate sampleRate, Audio::Opus::Channels channels) { 11 | frameSize_ = getFrameSize(sampleRate); 12 | 13 | int error{}; 14 | encoder_ = Encoder( 15 | opus_encoder_create(static_cast(sampleRate), static_cast(channels), OPUS_APPLICATION_AUDIO, &error), 16 | EncoderDeleter() 17 | ); 18 | if (OPUS_OK != error || nullptr == encoder_) { 19 | Audio::processError(error, Audio::Location::ENCODER_CREATE); 20 | } 21 | auto ret = opus_encoder_ctl(encoder_.get(), OPUS_SET_BITRATE(static_cast(compression))); 22 | if (ret != OPUS_OK) { 23 | Audio::processError(ret, Audio::Location::ENCODER_SET_BITRATE); 24 | }; 25 | } 26 | 27 | int EncoderOpus::encode(const char* pcmAudio, char* encodedPacket) { 28 | const opus_int32 encodeResult = opus_encode(encoder_.get(), reinterpret_cast(pcmAudio), frameSize_, 29 | reinterpret_cast(encodedPacket), Audio::Opus::maxPacketSize); 30 | // If DTX is on and the return value is 2 bytes or less, then the packet does not need to be transmitted. 31 | if (encodeResult >= 0 && encodeResult <= 2) { 32 | return 0; 33 | } 34 | if (encodeResult < 0 || encodeResult > Audio::Opus::maxPacketSize) { 35 | Audio::processError(encodeResult, Audio::Location::ENCODER_ENCODE); 36 | } 37 | return encodeResult; 38 | } 39 | 40 | int EncoderOpus::getFrameSize(Audio::Opus::SampleRate sampleRate) { 41 | return Audio::Opus::frameLength * static_cast(sampleRate) / 1000; 42 | } 43 | 44 | int EncoderOpus::getInputSize(int frameSize, Audio::Opus::Channels channels) { 45 | return frameSize * static_cast(channels) * 2; // 2 is sample size in bytes 46 | } 47 | 48 | //---EncoderDeleter--- 49 | 50 | void EncoderOpus::EncoderDeleter::operator()(OpusEncoder* enc) const { 51 | opus_encoder_destroy(enc); 52 | } 53 | -------------------------------------------------------------------------------- /SoundRemote/EncoderOpus.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "AudioUtil.h" 6 | 7 | struct OpusEncoder; 8 | 9 | class EncoderOpus { 10 | public: 11 | EncoderOpus(Audio::Compression compression, Audio::Opus::SampleRate sampleRate, Audio::Opus::Channels channels); 12 | 13 | /// 14 | /// Encodes a frame of PCM audio. 15 | /// 16 | /// - input signal in 16 bit signed int format. 17 | /// Use EncoderOpus::getInputSize() to get the required size. 18 | /// - buffer to contain the encoded packet. 19 | /// Use Audio::Opus::maxPacketSize to get the recommended buffer size. 20 | /// Encoded packet length in bytes. If the return value is 0 encoded packet does not need to be transmitted (DTX). 21 | int encode(const char* pcmAudio, char* encodedPacket); 22 | static int getFrameSize(Audio::Opus::SampleRate sampleRate); 23 | static int getInputSize(int frameSize, Audio::Opus::Channels channels); 24 | private: 25 | struct EncoderDeleter { 26 | void operator()(OpusEncoder* enc) const; 27 | }; 28 | using Encoder = std::unique_ptr; 29 | 30 | Encoder encoder_; 31 | // Number of samples per frame 32 | int frameSize_; 33 | 34 | EncoderOpus(const EncoderOpus&) = delete; 35 | EncoderOpus& operator= (const EncoderOpus&) = delete; 36 | }; 37 | -------------------------------------------------------------------------------- /SoundRemote/Keystroke.cpp: -------------------------------------------------------------------------------- 1 | #include "Keystroke.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | Keystroke::Keystroke(int key, int mods): key_(key) { 8 | if (mods == 0) 9 | return; 10 | if (mods & static_cast(ModKey::Win)) 11 | mods_.insert(ModKeyVk::Win); 12 | if (mods & static_cast(ModKey::Ctrl)) 13 | mods_.insert(ModKeyVk::Ctrl); 14 | if (mods & static_cast(ModKey::Shift)) 15 | mods_.insert(ModKeyVk::Shift); 16 | if (mods & static_cast(ModKey::Alt)) 17 | mods_.insert(ModKeyVk::Alt); 18 | } 19 | 20 | void Keystroke::emulate() const { 21 | const auto keyCount = mods_.size() + 1; 22 | const auto inputLen = keyCount * 2; 23 | std::vector inputs{ inputLen }; 24 | 25 | // All inputs 26 | for (auto i = 0; i < inputLen; ++i) { 27 | inputs[i].type = INPUT_KEYBOARD; 28 | } 29 | // Second half of the inputs is KEYUP 30 | for (auto i = keyCount; i < inputLen; ++i) { 31 | inputs[i].ki.dwFlags = KEYEVENTF_KEYUP; 32 | } 33 | 34 | // Set all but the middle pair of inputs to mods 35 | int index = 0; 36 | for (ModKeyVk modKey : mods_) { 37 | inputs[index].ki.wVk = static_cast(modKey); 38 | inputs[inputLen - 1 - index].ki.wVk = static_cast(modKey); 39 | ++index; 40 | } 41 | 42 | // Set the pair of inputs in the middle to the main key 43 | inputs[inputLen / 2 - 1].ki.wVk = key_; 44 | inputs[inputLen / 2 - 1].ki.dwFlags = KEYEVENTF_EXTENDEDKEY; 45 | inputs[inputLen / 2].ki.wVk = key_; 46 | inputs[inputLen / 2].ki.dwFlags = KEYEVENTF_KEYUP | KEYEVENTF_EXTENDEDKEY; 47 | 48 | const int eventsInserted = SendInput(static_cast(inputLen), inputs.data(), sizeof(INPUT)); 49 | if (eventsInserted != inputLen) { 50 | //TODO: handle SendInput error 51 | } 52 | } 53 | 54 | std::wstring Keystroke::toString() const { 55 | std::vector keys; 56 | if (mods_.contains(ModKeyVk::Win)) { 57 | keys.emplace_back(L"Win"); 58 | } 59 | if (mods_.contains(ModKeyVk::Ctrl)) { 60 | keys.emplace_back(L"Ctrl"); 61 | } 62 | if (mods_.contains(ModKeyVk::Shift)) { 63 | keys.emplace_back(L"Shift"); 64 | } 65 | if (mods_.contains(ModKeyVk::Alt)) { 66 | keys.emplace_back(L"Alt"); 67 | } 68 | keys.emplace_back(getVkCodeDescription(key_)); 69 | std::wstring result; 70 | for (auto&& keyString: keys) { 71 | if (!result.empty()) { 72 | result.append(L" + "); 73 | } 74 | result.append(keyString); 75 | } 76 | return result; 77 | } 78 | 79 | std::wstring Keystroke::getVkCodeDescription(int vkCode) const { 80 | switch (vkCode) { 81 | case VK_BROWSER_BACK: 82 | return L"Browser Back"; 83 | case VK_BROWSER_FORWARD: 84 | return L"Browser Forward"; 85 | case VK_BROWSER_REFRESH: 86 | return L"Browser Refresh"; 87 | case VK_BROWSER_STOP: 88 | return L"Browser Stop"; 89 | case VK_BROWSER_SEARCH: 90 | return L"Browser Search"; 91 | case VK_BROWSER_FAVORITES: 92 | return L"Browser Favorites"; 93 | case VK_BROWSER_HOME: 94 | return L"Browser Home"; 95 | 96 | case VK_VOLUME_MUTE: 97 | return L"Mute"; 98 | case VK_VOLUME_DOWN: 99 | return L"Volume Down"; 100 | case VK_VOLUME_UP: 101 | return L"Volume Up"; 102 | case VK_MEDIA_NEXT_TRACK: 103 | return L"Next Track"; 104 | case VK_MEDIA_PREV_TRACK: 105 | return L"Previous Track"; 106 | case VK_MEDIA_STOP: 107 | return L"Stop Media"; 108 | case VK_MEDIA_PLAY_PAUSE: 109 | return L"Play/Pause Media"; 110 | 111 | case VK_LAUNCH_MAIL: 112 | return L"Start Mail"; 113 | case VK_LAUNCH_MEDIA_SELECT: 114 | return L"Select Media"; 115 | case VK_LAUNCH_APP1: 116 | return L"Start Application 1"; 117 | case VK_LAUNCH_APP2: 118 | return L"Start Application 2"; 119 | 120 | case VK_SLEEP: 121 | return L"Computer Sleep"; 122 | 123 | case VK_SNAPSHOT: 124 | return L"Print Screen"; 125 | case VK_PAUSE: 126 | return L"Pause"; 127 | 128 | default: 129 | break; 130 | } 131 | 132 | UINT scanCode = MapVirtualKeyW(vkCode, MAPVK_VK_TO_VSC); 133 | // Handle extended keys 134 | // https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#extended-key-flag 135 | switch (vkCode) { 136 | case VK_RCONTROL: case VK_RMENU: 137 | case VK_INSERT: case VK_DELETE: case VK_HOME: case VK_END: case VK_PRIOR: case VK_NEXT: 138 | case VK_LEFT: case VK_UP: case VK_RIGHT: case VK_DOWN: 139 | case VK_NUMLOCK: case VK_CANCEL: case VK_DIVIDE: 140 | case VK_LWIN: case VK_RWIN: case VK_APPS: 141 | scanCode |= KF_EXTENDED; 142 | break; 143 | } 144 | 145 | const auto descSize = 512; 146 | WCHAR desc[descSize]; 147 | int result = GetKeyNameTextW(scanCode << 16, desc, descSize); 148 | if (result == 0) { 149 | return L"Unknown key"; 150 | } 151 | return { desc, static_cast(result) }; 152 | } 153 | -------------------------------------------------------------------------------- /SoundRemote/Keystroke.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class Keystroke { 7 | public: 8 | /// 9 | /// Creates a Keystroke. 10 | /// 11 | /// A virtual-key code. The code must be a value in the range 1 to 254. 12 | /// Bit field of Keystroke::ModKey values. 13 | Keystroke(int key, int mods); 14 | 15 | /// 16 | /// Emulates the keystroke. 17 | /// 18 | void emulate() const; 19 | 20 | std::wstring toString() const; 21 | 22 | private: 23 | enum class ModKey { 24 | Win = 1, 25 | Ctrl = 1 << 1, 26 | Shift = 1 << 2, 27 | Alt = 1 << 3, 28 | }; 29 | enum class ModKeyVk { 30 | Win = 0x5B, 31 | Ctrl = 0x11, 32 | Shift = 0x10, 33 | Alt = 0x12 34 | }; 35 | 36 | std::wstring getVkCodeDescription(int vkCode) const; 37 | 38 | const int key_; 39 | std::unordered_set mods_; 40 | }; 41 | -------------------------------------------------------------------------------- /SoundRemote/NetDefines.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace Net { 8 | namespace Packet { 9 | // Header fields 10 | using SignatureType = uint16_t; 11 | using CategoryType = uint8_t; 12 | using SizeType = uint16_t; 13 | // Data fields 14 | using ProtocolVersionType = uint8_t; 15 | using RequestIdType = uint16_t; 16 | using CompressionType = uint8_t; 17 | using KeyType = uint8_t; 18 | using ModsType = uint8_t; 19 | using SequenceNumberType = uint32_t; 20 | constexpr int ackCustomDataSize = 4; 21 | // Header data 22 | constexpr int headerSize = sizeof SignatureType + sizeof CategoryType + sizeof SizeType; 23 | constexpr int signatureOffset = 0; 24 | constexpr int categoryOffset = sizeof SignatureType; 25 | constexpr int sizeOffset = sizeof SignatureType + sizeof CategoryType; 26 | constexpr int dataOffset = headerSize; 27 | // Packet data 28 | constexpr int keystrokeSize = sizeof KeyType + sizeof ModsType; 29 | constexpr int ackSize = sizeof RequestIdType + ackCustomDataSize; 30 | constexpr int ackCustomDataOffset = dataOffset + sizeof RequestIdType; 31 | constexpr int sequenceNumberSize = sizeof SequenceNumberType; 32 | constexpr int audioDataOffset = dataOffset + sequenceNumberSize; 33 | struct ConnectData { 34 | ProtocolVersionType protocol; 35 | RequestIdType requestId; 36 | CompressionType compression; 37 | static const int size = sizeof ProtocolVersionType + sizeof RequestIdType + sizeof CompressionType; 38 | }; 39 | struct SetFormatData { 40 | RequestIdType requestId; 41 | CompressionType compression; 42 | static const int size = sizeof RequestIdType + sizeof CompressionType; 43 | }; 44 | 45 | constexpr SignatureType protocolSignature = 0xA571u; 46 | 47 | enum class Category: CategoryType { 48 | Error = 0, 49 | Connect = 0x01u, 50 | Disconnect = 0x02u, 51 | SetFormat = 0x03u, 52 | Keystroke = 0x10u, 53 | AudioDataUncompressed = 0x20u, 54 | AudioDataOpus = 0x21u, 55 | ClientKeepAlive = 0x30u, 56 | ServerKeepAlive = 0x31u, 57 | Ack = 0xF0u 58 | }; 59 | } 60 | constexpr Packet::ProtocolVersionType protocolVersion = 1u; 61 | 62 | using Address = boost::asio::ip::address; 63 | 64 | constexpr uint16_t defaultServerPort = 15711u; 65 | constexpr uint16_t defaultClientPort = 15712u; 66 | 67 | constexpr int inputPacketSize = 1024; 68 | } 69 | -------------------------------------------------------------------------------- /SoundRemote/NetUtil.cpp: -------------------------------------------------------------------------------- 1 | #include "NetUtil.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace { 9 | uint32_t readUInt32B(const std::span& data, size_t offset) { 10 | assert((offset + 4) <= data.size_bytes()); 11 | return (static_cast(data[offset]) << 24) 12 | | (static_cast(data[offset + 1]) << 16) 13 | | (static_cast(data[offset + 2]) << 8) 14 | | static_cast(data[offset + 3]); 15 | } 16 | 17 | uint32_t readUInt32L(const std::span& data, size_t offset) { 18 | assert((offset + 4) <= data.size_bytes()); 19 | return static_cast(data[offset]) 20 | | (static_cast(data[offset + 1]) << 8) 21 | | (static_cast(data[offset + 2]) << 16) 22 | | (static_cast(data[offset + 3]) << 24); 23 | } 24 | 25 | uint16_t readUInt16B(const std::span& data, size_t offset) { 26 | assert((offset + 2) <= data.size_bytes()); 27 | return static_cast(data[offset]) << 8 28 | | (static_cast(data[offset + 1])); 29 | } 30 | 31 | uint16_t readUInt16L(const std::span& data, size_t offset) { 32 | assert((offset + 2) <= data.size_bytes()); 33 | return static_cast(data[offset]) 34 | | (static_cast(data[offset + 1]) << 8); 35 | } 36 | 37 | uint8_t readUInt8(const std::span& data, size_t offset) { 38 | assert((offset + 1) <= data.size_bytes()); 39 | return data[offset]; 40 | } 41 | 42 | void writeUInt32B(uint32_t value, const std::span& dest, size_t offset) { 43 | assert((offset + 4) <= dest.size_bytes()); 44 | dest[offset] = value >> 24; 45 | dest[offset + 1] = value >> 16; 46 | dest[offset + 2] = value >> 8; 47 | dest[offset + 3] = value >> 0; 48 | } 49 | 50 | void writeUInt16B(uint16_t value, const std::span& dest, size_t offset) { 51 | assert((offset + 2) <= dest.size_bytes()); 52 | dest[offset] = value >> 8; 53 | dest[offset + 1] = value >> 0; 54 | } 55 | 56 | void writeUInt16L(uint16_t value, const std::span& dest, size_t offset) { 57 | assert((offset + 2) <= dest.size_bytes()); 58 | dest[offset] = value >> 0; 59 | dest[offset + 1] = value >> 8; 60 | } 61 | 62 | void writeUInt8(uint8_t value, const std::span& dest, size_t offset) { 63 | assert((offset + 1) <= dest.size_bytes()); 64 | dest[offset] = value; 65 | } 66 | 67 | void writeHeader(Net::Packet::Category category, const std::span& packetData) { 68 | writeUInt16B(Net::Packet::protocolSignature, packetData, Net::Packet::signatureOffset); 69 | writeUInt8(static_cast(category), packetData, Net::Packet::categoryOffset); 70 | writeUInt16B(static_cast(packetData.size_bytes()), packetData, Net::Packet::sizeOffset); 71 | } 72 | 73 | void writeAck(Net::Packet::RequestIdType requestId, const std::span& packetData) { 74 | writeUInt16B(requestId, packetData, Net::Packet::dataOffset); 75 | } 76 | }; 77 | 78 | std::forward_list Net::getLocalAddresses() { 79 | // Allocate MIB_IPADDRTABLE 80 | std::vector buffer{ sizeof(MIB_IPADDRTABLE) }; 81 | MIB_IPADDRTABLE* addrTable = reinterpret_cast(buffer.data()); 82 | DWORD addrTableSize = 0; 83 | if (ERROR_INSUFFICIENT_BUFFER == GetIpAddrTable(addrTable, &addrTableSize, 0)) { 84 | buffer.resize(addrTableSize); 85 | addrTable = reinterpret_cast(buffer.data()); 86 | } 87 | 88 | // Fill MIB_IPADDRTABLE 89 | if (NO_ERROR != GetIpAddrTable(addrTable, &addrTableSize, 0)) { 90 | return {}; 91 | } 92 | 93 | // Get addresses from MIB_IPADDRTABLE 94 | std::forward_list result; 95 | // For an IPv4 address, buffer should be large enough to hold at least 16 characters. 96 | std::wstring addr(16, 0); 97 | IN_ADDR IPAddr{}; 98 | for (int i = addrTable->dwNumEntries - 1; i >= 0; --i) { 99 | IPAddr.S_un.S_addr = static_cast(addrTable->table[i].dwAddr); 100 | auto res = InetNtopW(AF_INET, &IPAddr, addr.data(), addr.size()); 101 | if (res == nullptr) { 102 | return {}; 103 | } 104 | // Trim extra 0s at the end 105 | result.push_front(addr.c_str()); 106 | } 107 | return result; 108 | } 109 | 110 | std::optional Net::compressionFromNetworkValue(Net::Packet::CompressionType compression) { 111 | switch (compression) { 112 | case 0: 113 | return Audio::Compression::none; 114 | case 1: 115 | return Audio::Compression::kbps_64; 116 | case 2: 117 | return Audio::Compression::kbps_128; 118 | case 3: 119 | return Audio::Compression::kbps_192; 120 | case 4: 121 | return Audio::Compression::kbps_256; 122 | case 5: 123 | return Audio::Compression::kbps_320; 124 | default: 125 | return std::nullopt; 126 | } 127 | } 128 | 129 | std::vector Net::createAudioPacket( 130 | Net::Packet::Category category, 131 | Net::Packet::SequenceNumberType sequenceNumber, 132 | const std::span& audioData 133 | ) { 134 | std::vector packet(Net::Packet::headerSize + Net::Packet::sequenceNumberSize + audioData.size_bytes()); 135 | std::span packetData{ packet.data(), packet.size() }; 136 | writeHeader(category, packetData); 137 | writeUInt32B(sequenceNumber, packetData, Net::Packet::dataOffset); 138 | std::copy_n(audioData.data(), audioData.size_bytes(), packet.data() + Net::Packet::audioDataOffset); 139 | return packet; 140 | } 141 | 142 | std::vector Net::createKeepAlivePacket() { 143 | std::vector packet(Net::Packet::headerSize); 144 | writeHeader(Net::Packet::Category::ServerKeepAlive, { packet.data(), packet.size() }); 145 | return packet; 146 | } 147 | 148 | std::vector Net::createDisconnectPacket() { 149 | std::vector packet(Net::Packet::headerSize); 150 | writeHeader(Net::Packet::Category::Disconnect, { packet.data(), packet.size() }); 151 | return packet; 152 | } 153 | 154 | std::vector Net::createAckConnectPacket(Net::Packet::RequestIdType requestId) { 155 | std::vector packet(Net::Packet::headerSize + Net::Packet::ackSize); 156 | std::span packetData{ packet.data(), packet.size() }; 157 | writeHeader(Net::Packet::Category::Ack, packetData); 158 | writeAck(requestId, packetData); 159 | writeUInt8(Net::protocolVersion, packetData, Net::Packet::ackCustomDataOffset); 160 | return packet; 161 | } 162 | 163 | std::vector Net::createAckSetFormatPacket(Net::Packet::RequestIdType requestId) { 164 | std::vector packet(Net::Packet::headerSize + Net::Packet::ackSize); 165 | std::span packetData{ packet.data(), packet.size() }; 166 | writeHeader(Net::Packet::Category::Ack, packetData); 167 | writeAck(requestId, packetData); 168 | return packet; 169 | } 170 | 171 | Net::Packet::Category Net::getPacketCategory(const std::span& packet) { 172 | if (packet.size_bytes() < Packet::headerSize) { 173 | return Net::Packet::Category::Error; 174 | } 175 | const Packet::SignatureType signature = readUInt16B(packet, 0); 176 | if (signature != Packet::protocolSignature) { 177 | return Net::Packet::Category::Error; 178 | } 179 | return static_cast(readUInt8(packet, Net::Packet::categoryOffset)); 180 | } 181 | 182 | std::optional Net::getKeystroke(const std::span& packet) { 183 | if (static_cast(packet.size()) < Packet::dataOffset + Packet::keystrokeSize) { 184 | return std::nullopt; 185 | } 186 | int offset = Packet::dataOffset; 187 | Packet::KeyType key = readUInt8(packet, offset); 188 | offset += sizeof(key); 189 | Packet::ModsType mods = readUInt8(packet, offset); 190 | return Keystroke{ static_cast(key), static_cast(mods) }; 191 | } 192 | 193 | std::optional Net::getConnectData(const std::span& packet) { 194 | if (static_cast(packet.size()) < Packet::dataOffset + Packet::ConnectData::size) { 195 | return std::nullopt; 196 | } 197 | int offset = Packet::dataOffset; 198 | Net::Packet::ConnectData data{}; 199 | data.protocol = readUInt8(packet, offset); 200 | offset += sizeof(Net::Packet::ProtocolVersionType); 201 | data.requestId = readUInt16B(packet, offset); 202 | offset += sizeof(Net::Packet::RequestIdType); 203 | data.compression = readUInt8(packet, offset); 204 | return data; 205 | } 206 | 207 | std::optional Net::getSetFormatData(const std::span& packet) { 208 | if (static_cast(packet.size()) < Packet::dataOffset + Packet::SetFormatData::size) { 209 | return std::nullopt; 210 | } 211 | int offset = Packet::dataOffset; 212 | Net::Packet::SetFormatData data{}; 213 | data.requestId = readUInt16B(packet, offset); 214 | offset += sizeof(Net::Packet::RequestIdType); 215 | data.compression = readUInt8(packet, offset); 216 | return data; 217 | } 218 | -------------------------------------------------------------------------------- /SoundRemote/NetUtil.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "AudioUtil.h" 8 | #include "Keystroke.h" 9 | #include "NetDefines.h" 10 | 11 | namespace Net { 12 | /// 13 | /// Gets string representations of the IPv4 IP addresses. 14 | /// 15 | /// List of addresses or an empty list if an error occurs. 16 | std::forward_list getLocalAddresses(); 17 | 18 | /// 19 | /// Converts a compression value used in the network protocol to a value of the Audio::Compression enum. 20 | /// 21 | /// Network protocol compression value 22 | /// std::optional containing an Audio::Compression value or std::nullopt if the passed 23 | /// argument is not a valid compression value. 24 | std::optional compressionFromNetworkValue(Net::Packet::CompressionType compression); 25 | 26 | std::vector createAudioPacket( 27 | Net::Packet::Category category, 28 | Net::Packet::SequenceNumberType sequenceNumber, 29 | const std::span& audioData 30 | ); 31 | std::vector createKeepAlivePacket(); 32 | std::vector createDisconnectPacket(); 33 | std::vector createAckConnectPacket(Net::Packet::RequestIdType requestId); 34 | std::vector createAckSetFormatPacket(Net::Packet::RequestIdType requestId); 35 | 36 | Net::Packet::Category getPacketCategory(const std::span& packet); 37 | std::optional getKeystroke(const std::span& packet); 38 | std::optional getConnectData(const std::span& packet); 39 | std::optional getSetFormatData(const std::span& packet); 40 | }; 41 | -------------------------------------------------------------------------------- /SoundRemote/Server.cpp: -------------------------------------------------------------------------------- 1 | #include "Server.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "Clients.h" 7 | #include "NetUtil.h" 8 | #include "Util.h" 9 | 10 | using boost::asio::ip::udp; 11 | using boost::asio::awaitable; 12 | using boost::asio::co_spawn; 13 | using boost::asio::detached; 14 | using boost::asio::use_awaitable; 15 | using namespace std::chrono_literals; 16 | using namespace std::placeholders; 17 | 18 | Server::Server(int clientPort, int serverPort, boost::asio::io_context& ioContext, std::shared_ptr clients) : 19 | clientPort_(clientPort), 20 | clients_(clients), 21 | socketSend_(ioContext, udp::v4()), 22 | socketReceive_(ioContext, udp::endpoint(udp::v4(), serverPort)), 23 | maintainenanceTimer_(ioContext) { 24 | 25 | //co_spawn(ioContext, receive(std::move(socket)), detached); 26 | co_spawn(ioContext, receive(socketReceive_), detached); 27 | startMaintenanceTimer(); 28 | } 29 | 30 | Server::~Server() { 31 | socketReceive_.shutdown(udp::socket::shutdown_receive); 32 | socketReceive_.close(); 33 | socketSend_.shutdown(udp::socket::shutdown_send); 34 | socketSend_.close(); 35 | } 36 | 37 | void Server::onClientsUpdate(std::forward_list clients) { 38 | std::unordered_map> newClients; 39 | for (auto&& client: clients) { 40 | if (!newClients.contains(client.compression)) { 41 | newClients[client.compression] = std::forward_list(); 42 | } 43 | newClients[client.compression].push_front(client.address); 44 | } 45 | clientsCache_ = std::move(newClients); 46 | } 47 | 48 | void Server::sendAudio( 49 | Audio::Compression compression, 50 | Net::Packet::SequenceNumberType sequenceNumber, 51 | std::vector data 52 | ) { 53 | auto category = compression == Audio::Compression::none ? 54 | Net::Packet::Category::AudioDataUncompressed : 55 | Net::Packet::Category::AudioDataOpus; 56 | auto packet = std::make_shared>( 57 | Net::createAudioPacket(category, sequenceNumber, { data.data(), data.size() }) 58 | ); 59 | for (auto&& address : clientsCache_[compression]) { 60 | send(address, packet); 61 | } 62 | } 63 | 64 | void Server::sendDisconnectBlocking() { 65 | if (clientsCache_.empty()) { return; } 66 | auto destination = udp::endpoint(udp::v4(), clientPort_); 67 | auto packet = std::make_shared>(Net::createDisconnectPacket()); 68 | for (auto&& [compression, addresses] : clientsCache_) { 69 | for (auto&& address : addresses) { 70 | destination.address(address); 71 | socketSend_.send_to(boost::asio::buffer(packet->data(), packet->size()), destination); 72 | } 73 | } 74 | } 75 | 76 | void Server::setKeystrokeCallback(KeystrokeCallback callback) { 77 | keystrokeCallback_ = callback; 78 | } 79 | 80 | awaitable Server::receive(udp::socket& socket) { 81 | //throw std::exception("Server::receive"); 82 | std::array datagram{}; 83 | udp::endpoint sender; 84 | //Have to handle errors here or write custom completion handler for co_spawn() 85 | try { 86 | for (;;) { 87 | auto nBytes = co_await socket.async_receive_from(boost::asio::buffer(datagram), sender, use_awaitable); 88 | std::span receivedData = { datagram.data(), nBytes }; 89 | auto category = Net::getPacketCategory(receivedData); 90 | switch (category) { 91 | case Net::Packet::Category::Connect: 92 | processConnect(sender.address(), receivedData); 93 | break; 94 | case Net::Packet::Category::Disconnect: 95 | processDisconnect(sender.address()); 96 | break; 97 | case Net::Packet::Category::SetFormat: 98 | processSetFormat(sender.address(), receivedData); 99 | break; 100 | case Net::Packet::Category::Keystroke: 101 | processKeystroke(receivedData); 102 | break; 103 | case Net::Packet::Category::ClientKeepAlive: 104 | processKeepAlive(sender.address()); 105 | break; 106 | default: 107 | break; 108 | } 109 | } 110 | } 111 | catch (const boost::system::system_error& se) { 112 | if (se.code() != boost::asio::error::operation_aborted) { 113 | Util::showError(Util::makeAppErrorText("Receive", se.what())); 114 | std::exit(EXIT_FAILURE); 115 | } 116 | } 117 | catch (const std::exception& e) { 118 | Util::showError(Util::makeAppErrorText("Receive", e.what())); 119 | std::exit(EXIT_FAILURE); 120 | } 121 | catch (...) { 122 | Util::showError("Receive: unknown error"); 123 | std::exit(EXIT_FAILURE); 124 | } 125 | } 126 | 127 | void Server::processConnect(const Net::Address& address, const std::span& packet) { 128 | const auto connectData = Net::getConnectData(packet); 129 | if (!connectData) { return; } 130 | auto compression = Net::compressionFromNetworkValue(connectData->compression); 131 | if (!compression) { return; } 132 | clients_->add(address, *compression); 133 | 134 | send(address, std::make_shared>( 135 | Net::createAckConnectPacket(connectData->requestId) 136 | )); 137 | } 138 | 139 | void Server::processDisconnect(const Net::Address& address) { 140 | clients_->remove(address); 141 | } 142 | 143 | void Server::processSetFormat(const Net::Address& address, const std::span& packet) { 144 | const auto setFormatData = Net::getSetFormatData(packet); 145 | if (!setFormatData) { return; } 146 | auto compression = Net::compressionFromNetworkValue(setFormatData->compression); 147 | if (!compression) { return; } 148 | clients_->setCompression(address, *compression); 149 | 150 | send(address, std::make_shared>( 151 | Net::createAckSetFormatPacket(setFormatData->requestId) 152 | )); 153 | } 154 | 155 | void Server::processKeystroke(const std::span& packet) const { 156 | const std::optional keystroke = Net::getKeystroke(packet); 157 | if (!keystroke) { return; } 158 | keystroke->emulate(); 159 | if (keystrokeCallback_) { 160 | keystrokeCallback_(*keystroke); 161 | } 162 | } 163 | 164 | void Server::processKeepAlive(const Net::Address& address) const { 165 | clients_->keep(address); 166 | } 167 | 168 | void Server::send(const Net::Address& address, const std::shared_ptr> packet) { 169 | auto destination = udp::endpoint(address, clientPort_); 170 | destination.address(address); 171 | socketSend_.async_send_to(boost::asio::buffer(packet->data(), packet->size()), destination, 172 | std::bind(&Server::handleSend, this, packet, _1, _2)); 173 | } 174 | 175 | // std::shared_ptr with the packet is passed to keep data alive until the handler call 176 | void Server::handleSend(const std::shared_ptr> packet, const boost::system::error_code& ec, std::size_t bytes) { 177 | if (ec) { 178 | throw std::runtime_error(Util::makeAppErrorText("Server send", ec.what())); 179 | } 180 | } 181 | 182 | void Server::startMaintenanceTimer() { 183 | maintainenanceTimer_.expires_after(1s); 184 | maintainenanceTimer_.async_wait(std::bind(&Server::maintain, this, std::placeholders::_1)); 185 | } 186 | 187 | void Server::maintain(boost::system::error_code ec) { 188 | if (ec) { 189 | if (ec == boost::asio::error::operation_aborted) { 190 | return; 191 | } else { 192 | throw std::runtime_error(Util::makeAppErrorText("Timer maintain", ec.what())); 193 | } 194 | } 195 | keepalive(); 196 | clients_->maintain(); 197 | 198 | startMaintenanceTimer(); 199 | } 200 | 201 | void Server::keepalive() { 202 | if (clientsCache_.empty()) { return; } 203 | auto packet = std::make_shared>(Net::createKeepAlivePacket()); 204 | for (auto&& [compression, addresses] : clientsCache_) { 205 | for (auto&& address : addresses) { 206 | send(address, packet); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /SoundRemote/Server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "AudioUtil.h" 14 | #include "Keystroke.h" 15 | #include "NetDefines.h" 16 | 17 | class Clients; 18 | struct ClientInfo; 19 | 20 | class Server { 21 | public: 22 | using KeystrokeCallback = std::function; 23 | 24 | Server(int clientPort, int serverPort, boost::asio::io_context& ioContext, std::shared_ptr clients); 25 | ~Server(); 26 | void onClientsUpdate(std::forward_list clients); 27 | void sendAudio( 28 | Audio::Compression compression, 29 | Net::Packet::SequenceNumberType sequenceNumber, 30 | std::vector data 31 | ); 32 | /* 33 | * Sends disconnect packet to all the clients blocking the current thread. 34 | * 35 | * @throws boost::system::system_error Thrown on failure. 36 | */ 37 | void sendDisconnectBlocking(); 38 | void setKeystrokeCallback(KeystrokeCallback callback); 39 | private: 40 | boost::asio::awaitable receive(boost::asio::ip::udp::socket& socket); 41 | void processConnect(const Net::Address& address, const std::span& packet); 42 | void processDisconnect(const Net::Address& address); 43 | void processSetFormat(const Net::Address& address, const std::span& packet); 44 | void processKeystroke(const std::span& packet) const; 45 | void processKeepAlive(const Net::Address& address) const; 46 | void send(const Net::Address& address, const std::shared_ptr> packet); 47 | void handleSend(const std::shared_ptr> packet, const boost::system::error_code& ec, std::size_t bytes); 48 | void keepalive(); 49 | 50 | void startMaintenanceTimer(); 51 | void maintain(boost::system::error_code ec); 52 | 53 | boost::asio::ip::udp::socket socketSend_; 54 | boost::asio::ip::udp::socket socketReceive_; 55 | boost::asio::steady_timer maintainenanceTimer_; 56 | int clientPort_; 57 | KeystrokeCallback keystrokeCallback_; 58 | std::shared_ptr clients_; 59 | std::unordered_map> clientsCache_; 60 | }; 61 | -------------------------------------------------------------------------------- /SoundRemote/Settings.cpp: -------------------------------------------------------------------------------- 1 | #include "Settings.h" 2 | 3 | // Settings' names must be underscore. 4 | const std::string Settings::ServerPort{ "server_port" }; 5 | const std::string Settings::ClientPort{ "client_port" }; 6 | -------------------------------------------------------------------------------- /SoundRemote/Settings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class Settings { 8 | public: 9 | using Value = std::variant; 10 | // This enum must contain all the types supported by the Settings::Value. 11 | // Enum's values must be in the same order and hold the same zero-based index. 12 | enum class ValueType { 13 | Int = 0 14 | }; 15 | 16 | // Supported options 17 | static const std::string ServerPort; 18 | static const std::string ClientPort; 19 | 20 | virtual ~Settings() {}; 21 | template 22 | std::optional get(const std::string& settingName) const; 23 | protected: 24 | virtual std::optional getValue(const std::string& settingName) const = 0; 25 | }; 26 | 27 | template 28 | inline std::optional Settings::get(const std::string& settingName) const { 29 | auto valueOptional = getValue(settingName); 30 | if (!valueOptional) { 31 | return {}; 32 | } 33 | if (!std::holds_alternative(*valueOptional)) { 34 | return {}; 35 | } 36 | return std::optional(std::get(*valueOptional)); 37 | } 38 | -------------------------------------------------------------------------------- /SoundRemote/SettingsImpl.cpp: -------------------------------------------------------------------------------- 1 | #include "SettingsImpl.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "Util.h" 8 | 9 | std::string toLower(std::string s) { 10 | std::transform(s.begin(), s.end(), s.begin(), 11 | [](unsigned char c) { return std::tolower(c); } 12 | ); 13 | return s; 14 | } 15 | 16 | SettingsImpl::SettingsImpl() {} 17 | 18 | std::optional SettingsImpl::getValue(const std::string& settingName) const { 19 | if (settings_.contains(settingName)) { 20 | return settings_.at(settingName); 21 | } 22 | if (defaults_.contains(settingName)) { 23 | return defaults_.at(settingName); 24 | } 25 | return {}; 26 | } 27 | 28 | void SettingsImpl::addSetting(const std::string& name, Settings::Value defautlValue) { 29 | defaults_[name] = defautlValue; 30 | } 31 | 32 | void SettingsImpl::setFile(const std::string& fileName) { 33 | settings_.clear(); 34 | 35 | std::fstream fs{ fileName, std::ios::in }; 36 | if (fs.is_open()) { 37 | bool parseSuccess = true; 38 | // Read until an eof/error encountered or all settings are read. 39 | while (fs.good() && parseSuccess && (settings_.size() < defaults_.size())) { 40 | std::string lineStr; 41 | std::getline(fs, lineStr); 42 | if (fs.good() || fs.eof()) { 43 | parseSuccess = parseLine(lineStr, defaults_, settings_); 44 | } 45 | } 46 | const bool readSuccessful = (fs.good() || fs.eof()) && (settings_.size() == defaults_.size()); 47 | if (readSuccessful) { 48 | return; 49 | } 50 | } 51 | writeToFile(fileName, defaults_, settings_); 52 | } 53 | 54 | bool SettingsImpl::parseLine(const std::string& line, const SettingsMap& defaults, SettingsMap& settings) { 55 | // Blank 56 | if (line.empty()) { 57 | return true; 58 | } 59 | // Comment 60 | if (line.at(0) == ';') { 61 | return true; 62 | } 63 | // Section 64 | if (std::regex_match(line, section_)) { 65 | return true; 66 | } 67 | // Property 68 | std::smatch match; 69 | if (std::regex_search(line, match, property_)) { 70 | // Settings' names are stored in lowercase. 71 | const std::string propName = toLower(match[1]); 72 | const std::string propValue = match[2]; 73 | if (!defaults.contains(propName)) { 74 | return false; 75 | } 76 | 77 | const auto valueTypeIndex = defaults.at(propName).index(); 78 | Value settingValue; 79 | try { 80 | switch (valueTypeIndex) { 81 | case static_cast(ValueType::Int): 82 | settingValue = std::stoi(propValue); 83 | break; 84 | default: 85 | return false; 86 | } 87 | } 88 | catch (...) { 89 | return false; 90 | } 91 | settings[propName] = settingValue; 92 | return true; 93 | } 94 | // Failed to parse 95 | return false; 96 | } 97 | 98 | void SettingsImpl::writeToFile(const std::string& fileName, const SettingsMap& defaults, const SettingsMap& settings) { 99 | std::fstream fs{ fileName, std::ios::out }; 100 | if (!fs.is_open()) { 101 | //todo: print full path 102 | Util::showError("Can't create settings file."); 103 | return; 104 | } 105 | for (auto&& p : defaults) { 106 | const auto& name = p.first; 107 | Value value; 108 | if (settings.contains(name)) { 109 | value = settings.at(name); 110 | } else { 111 | value = p.second; 112 | } 113 | std::string valueStr; 114 | switch (value.index()) { 115 | case static_cast(ValueType::Int): 116 | valueStr = std::to_string(std::get(value)); 117 | break; 118 | default: 119 | break; 120 | } 121 | fs << name << '=' << valueStr << std::endl; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /SoundRemote/SettingsImpl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "Settings.h" 9 | 10 | class SettingsImpl : public Settings { 11 | public: 12 | SettingsImpl(); 13 | void addSetting(const std::string& name, Value defautlValue); 14 | void setFile(const std::string& fileName); 15 | protected: 16 | std::optional getValue(const std::string& settingName) const override; 17 | private: 18 | using SettingsMap = std::unordered_map; 19 | 20 | SettingsMap settings_; 21 | SettingsMap defaults_; 22 | const std::regex section_{ "^\\[.*\\]$" }; 23 | const std::regex property_{ "^\\s*(.+?)\\s*=\\s*(.+?)\\s*$" }; 24 | 25 | bool parseLine(const std::string& line, const SettingsMap& defaults, SettingsMap& settings); 26 | void writeToFile(const std::string& fileName, const SettingsMap& defaults, const SettingsMap& settings); 27 | }; 28 | -------------------------------------------------------------------------------- /SoundRemote/SoundRemote.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoundRemote/server-windows/7bfa0dfb7785c66458f5052807fb38bc2b263fec/SoundRemote/SoundRemote.ico -------------------------------------------------------------------------------- /SoundRemote/SoundRemote.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoundRemote/server-windows/7bfa0dfb7785c66458f5052807fb38bc2b263fec/SoundRemote/SoundRemote.rc -------------------------------------------------------------------------------- /SoundRemote/SoundRemote.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 16.0 23 | Win32Proj 24 | {d85222ef-3a69-4496-8f74-1e7e652cecff} 25 | SoundRemote 26 | 10.0 27 | 28 | 29 | 30 | Application 31 | true 32 | v143 33 | Unicode 34 | 35 | 36 | Application 37 | false 38 | v143 39 | true 40 | Unicode 41 | 42 | 43 | Application 44 | true 45 | v143 46 | Unicode 47 | 48 | 49 | Application 50 | false 51 | v143 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | true 75 | $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ 76 | $(SolutionDir)$(Platform)\$(Configuration)\ 77 | 78 | 79 | false 80 | $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ 81 | $(SolutionDir)$(Platform)\$(Configuration)\ 82 | 83 | 84 | true 85 | $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ 86 | 87 | 88 | false 89 | $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ 90 | 91 | 92 | 93 | Level3 94 | true 95 | WIN32;_WINDOWS;_WIN32_WINNT=0x0601;WIN32_LEAN_AND_MEAN;_DEBUG;BOOST_JSON_NO_LIB;BOOST_CONTAINER_NO_LIB;%(PreprocessorDefinitions) 96 | true 97 | $(SolutionDir)include;%BOOST_ROOT%;%(AdditionalIncludeDirectories) 98 | stdcpp20 99 | true 100 | 101 | 102 | Windows 103 | true 104 | $(SolutionDir)lib\opus\$(Platform);%(AdditionalLibraryDirectories) 105 | opus.lib;mfplat.lib;ws2_32.lib;version.lib;winhttp.lib;iphlpapi.lib;%(AdditionalDependencies) 106 | msvcrt.lib;%(IgnoreSpecificDefaultLibraries) 107 | 108 | 109 | 110 | 111 | Level3 112 | true 113 | true 114 | true 115 | WIN32;_WINDOWS;_WIN32_WINNT=0x0601;WIN32_LEAN_AND_MEAN;NDEBUG;BOOST_JSON_NO_LIB;BOOST_CONTAINER_NO_LIB;%(PreprocessorDefinitions) 116 | true 117 | stdcpp20 118 | $(SolutionDir)include;%BOOST_ROOT%;%(AdditionalIncludeDirectories) 119 | true 120 | 121 | 122 | Windows 123 | true 124 | true 125 | true 126 | $(SolutionDir)lib\opus\$(Platform);%(AdditionalLibraryDirectories) 127 | opus.lib;mfplat.lib;ws2_32.lib;version.lib;winhttp.lib;iphlpapi.lib;%(AdditionalDependencies) 128 | 129 | 130 | 131 | 132 | Level3 133 | true 134 | _WINDOWS;_WIN32_WINNT=0x0601;WIN32_LEAN_AND_MEAN;_DEBUG;BOOST_JSON_NO_LIB;BOOST_CONTAINER_NO_LIB;%(PreprocessorDefinitions) 135 | true 136 | $(SolutionDir)include;%BOOST_ROOT%;%(AdditionalIncludeDirectories) 137 | true 138 | stdcpp20 139 | 140 | 141 | Windows 142 | true 143 | opus.lib;mfplat.lib;ws2_32.lib;version.lib;winhttp.lib;iphlpapi.lib;%(AdditionalDependencies) 144 | $(SolutionDir)lib\opus\$(Platform);%(AdditionalLibraryDirectories) 145 | msvcrt.lib;%(IgnoreSpecificDefaultLibraries) 146 | 147 | 148 | 149 | 150 | Level3 151 | true 152 | true 153 | true 154 | _WINDOWS;_WIN32_WINNT=0x0601;WIN32_LEAN_AND_MEAN;NDEBUG;BOOST_JSON_NO_LIB;BOOST_CONTAINER_NO_LIB;%(PreprocessorDefinitions) 155 | true 156 | $(SolutionDir)include;%BOOST_ROOT%;%(AdditionalIncludeDirectories) 157 | true 158 | stdcpp20 159 | 160 | 161 | Windows 162 | true 163 | true 164 | true 165 | opus.lib;mfplat.lib;ws2_32.lib;version.lib;winhttp.lib;iphlpapi.lib;%(AdditionalDependencies) 166 | $(SolutionDir)lib\opus\$(Platform);%(AdditionalLibraryDirectories) 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /SoundRemote/SoundRemote.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | {0ad23db1-a994-4c0b-b9ef-0fe298f6895c} 18 | 19 | 20 | {f280d87d-9b6d-47ff-ad92-6c04c73e4bd5} 21 | 22 | 23 | {1775683c-62d1-4290-99a8-f06f544e2e52} 24 | 25 | 26 | {c510c1df-3532-4417-b882-eaf9578d8f75} 27 | 28 | 29 | 30 | 31 | Header Files 32 | 33 | 34 | Header Files 35 | 36 | 37 | Header Files 38 | 39 | 40 | Header Files 41 | 42 | 43 | Header Files 44 | 45 | 46 | Header Files\Network 47 | 48 | 49 | Header Files\Audio 50 | 51 | 52 | Header Files\Network 53 | 54 | 55 | Header Files 56 | 57 | 58 | Header Files\Audio 59 | 60 | 61 | Header Files\Audio 62 | 63 | 64 | Header Files\Audio 65 | 66 | 67 | Header Files\Audio 68 | 69 | 70 | Header Files 71 | 72 | 73 | Header Files 74 | 75 | 76 | Header Files 77 | 78 | 79 | Header Files 80 | 81 | 82 | Header Files\Network 83 | 84 | 85 | Header Files 86 | 87 | 88 | 89 | 90 | Source Files 91 | 92 | 93 | Source Files 94 | 95 | 96 | Source Files\Network 97 | 98 | 99 | Source Files\Audio 100 | 101 | 102 | Source Files\Network 103 | 104 | 105 | Source Files 106 | 107 | 108 | Source Files\Audio 109 | 110 | 111 | Source Files\Audio 112 | 113 | 114 | Source Files\Audio 115 | 116 | 117 | Source Files\Audio 118 | 119 | 120 | Source Files 121 | 122 | 123 | Source Files 124 | 125 | 126 | Source Files 127 | 128 | 129 | Source Files 130 | 131 | 132 | Source Files 133 | 134 | 135 | 136 | 137 | Resource Files 138 | 139 | 140 | 141 | 142 | Resource Files 143 | 144 | 145 | Resource Files 146 | 147 | 148 | Resource Files 149 | 150 | 151 | -------------------------------------------------------------------------------- /SoundRemote/SoundRemoteApp.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "resource.h" 12 | 13 | class MuteButton; 14 | class CapturePipe; 15 | class Clients; 16 | struct ClientInfo; 17 | class Keystroke; 18 | class Server; 19 | class Settings; 20 | class UpdateChecker; 21 | 22 | class SoundRemoteApp { 23 | public: 24 | SoundRemoteApp(_In_ HINSTANCE hInstance); 25 | ~SoundRemoteApp(); 26 | static std::unique_ptr create(_In_ HINSTANCE hInstance); 27 | int exec(int nCmdShow); 28 | private: 29 | HINSTANCE hInst_ = nullptr; // current instance 30 | // Strings 31 | std::wstring mainWindowTitle_; 32 | std::wstring serverAddressesLabel_; 33 | std::wstring defaultRenderDeviceLabel_; 34 | std::wstring defaultCaptureDeviceLabel_; 35 | std::wstring clientListLabel_; 36 | std::wstring keystrokeListLabel_; 37 | std::wstring muteButtonText_; 38 | std::wstring updateCheckTitle_; 39 | std::wstring updateCheckFound_; 40 | std::wstring updateCheckNotFound_; 41 | std::wstring updateCheckError_; 42 | // Controls 43 | HWND mainWindow_ = nullptr; 44 | HWND deviceComboBox_ = nullptr; 45 | HWND clientsList_ = nullptr; 46 | HWND addressButton_ = nullptr; 47 | HWND peakMeterProgress_ = nullptr; 48 | HWND keystrokes_ = nullptr; 49 | std::unique_ptr muteButton_; 50 | // Data 51 | std::wstring currentDeviceId_; 52 | std::unordered_map deviceIds_; // 53 | // Utility 54 | boost::asio::io_context ioContext_; 55 | std::unique_ptr ioContextThread_; 56 | std::shared_ptr server_; 57 | std::unique_ptr capturePipe_; 58 | std::shared_ptr settings_; 59 | std::shared_ptr clients_; 60 | std::unique_ptr updateChecker_; 61 | 62 | bool initInstance(int nCmdShow); 63 | // UI related 64 | void initStrings(); 65 | void initInterface(HWND hWndParent); 66 | void initControls(); 67 | void startPeakMeter(); 68 | void stopPeakMeter(); 69 | /// 70 | /// Adds devices for passed EDataFlow, including items for default devices. 71 | /// 72 | /// ComboBox to add to. 73 | /// Can be eRender, eCapture or eAll 74 | void addDevices(HWND comboBox, EDataFlow flow); 75 | /// 76 | /// Adds default device for EDataFlow::eRender or EDataFlow::eCapture. 77 | /// 78 | /// ComboBox to add to. 79 | /// Must be EDataFlow::eRender or EDataFlow::eCapture. 80 | void addDefaultDevice(HWND comboBox, EDataFlow flow); 81 | std::wstring getDeviceId(const int deviceIndex) const; 82 | long getCharHeight(HWND hWnd) const; 83 | 84 | // Description: 85 | // Creates a tooltip for a control. 86 | // Parameters: 87 | // toolWindow - window handle of the control to add the tooltip to. 88 | // text - string to use as the tooltip text. 89 | // parentWindow - parent window handle. 90 | // Returns: 91 | // The handle to the tooltip. 92 | HWND setTooltip(HWND toolWindow, PTSTR text, HWND parentWindow); 93 | std::wstring loadStringResource(UINT resourceId); 94 | void initSettings(); 95 | 96 | // Event handlers 97 | void onDeviceSelect(); 98 | void onClientListUpdate(std::forward_list clients); 99 | void onClientsUpdate(std::forward_list clients); 100 | void onAddressButtonClick() const; 101 | void updatePeakMeter(); 102 | void onReceiveKeystroke(const Keystroke& keystroke); 103 | void checkUpdates(); 104 | void onUpdateCheckFinish(WPARAM wParam, LPARAM lParam); 105 | void visitHomepage() const; 106 | 107 | /// 108 | /// Starts server and audio processing. 109 | /// 110 | void run(); 111 | void shutdown(); 112 | void changeCaptureDevice(const std::wstring& deviceId); 113 | void stopCapture(); 114 | void asioEventLoop(boost::asio::io_context& ctx); 115 | 116 | static LRESULT CALLBACK staticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); 117 | LRESULT wndProc(UINT message, WPARAM wParam, LPARAM lParam); 118 | static INT_PTR CALLBACK about(HWND, UINT, WPARAM, LPARAM); 119 | }; 120 | -------------------------------------------------------------------------------- /SoundRemote/UpdateChecker.cpp: -------------------------------------------------------------------------------- 1 | #include "UpdateChecker.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include "Util.h" 13 | 14 | // Preprocessor definitions added for the boost::json lib: 15 | // BOOST_JSON_NO_LIB 16 | // BOOST_CONTAINER_NO_LIB 17 | 18 | using namespace boost::json; 19 | 20 | namespace { 21 | class HttpHandleCloser { 22 | public: 23 | HttpHandleCloser(HINTERNET* pHandle): pHandle_(pHandle) {} 24 | ~HttpHandleCloser() { 25 | HINTERNET handle = *pHandle_; 26 | if (handle) { WinHttpCloseHandle(handle); } 27 | } 28 | private: 29 | HINTERNET* pHandle_; 30 | }; 31 | } 32 | 33 | UpdateChecker::UpdateChecker(HWND mainWindow): mainWindow_(mainWindow) { 34 | } 35 | 36 | void UpdateChecker::checkUpdates() { 37 | std::jthread worker(&UpdateChecker::checkWorker, this); 38 | worker.detach(); 39 | } 40 | 41 | std::string UpdateChecker::getVersion() const { 42 | TCHAR modulePath[MAX_PATH]; 43 | if (!GetModuleFileName(nullptr, modulePath, MAX_PATH)) { 44 | return {}; 45 | } 46 | 47 | DWORD dwHandle; 48 | auto versionInfoSize = GetFileVersionInfoSize(modulePath, &dwHandle); 49 | if (!versionInfoSize) { 50 | return {}; 51 | } 52 | 53 | std::vector buffer(versionInfoSize); 54 | if (!GetFileVersionInfo(modulePath, dwHandle, versionInfoSize, buffer.data())) { 55 | return {}; 56 | } 57 | 58 | struct { 59 | WORD language; 60 | WORD codePage; 61 | } *translate = nullptr; 62 | UINT uiSize; 63 | if (!VerQueryValue( 64 | buffer.data(), 65 | TEXT("\\VarFileInfo\\Translation"), 66 | (LPVOID*)&translate, 67 | &uiSize 68 | )) { 69 | return {}; 70 | } 71 | 72 | auto productVersionBlock = std::format( 73 | "\\StringFileInfo\\{:04x}{:04x}\\ProductVersion", 74 | translate[0].language, 75 | translate[0].codePage 76 | ); 77 | LPSTR version = nullptr; 78 | UINT versionSize; 79 | if (!VerQueryValueA( 80 | buffer.data(), 81 | productVersionBlock.c_str(), 82 | (LPVOID*)&version, 83 | &versionSize 84 | )) { 85 | return {}; 86 | } 87 | return std::string{ version }; 88 | } 89 | 90 | // https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release 91 | std::u8string UpdateChecker::getLatestRelease() const { 92 | BOOL results = FALSE; 93 | HINTERNET 94 | session = nullptr, 95 | connect = nullptr, 96 | request = nullptr; 97 | // Auto close HINTERNET handles 98 | HttpHandleCloser sessionCloser(&session); 99 | HttpHandleCloser connectCloser(&connect); 100 | HttpHandleCloser requestCloser(&request); 101 | 102 | session = WinHttpOpen( 103 | TEXT("SoundRemote server"), 104 | WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, 105 | WINHTTP_NO_PROXY_NAME, 106 | WINHTTP_NO_PROXY_BYPASS, 107 | WINHTTP_FLAG_SECURE_DEFAULTS 108 | ); 109 | if (!session) { return {}; } 110 | 111 | connect = WinHttpConnect( 112 | session, 113 | TEXT("api.github.com"), 114 | INTERNET_DEFAULT_HTTPS_PORT, 115 | 0 116 | ); 117 | if (!connect) { return {}; } 118 | 119 | auto acceptHeader = TEXT("application/vnd.github+json"); 120 | std::array acceptTypes{ acceptHeader, nullptr }; 121 | request = WinHttpOpenRequest( 122 | connect, 123 | nullptr, 124 | TEXT("/repos/soundremote/server-windows/releases/latest"), 125 | nullptr, 126 | WINHTTP_NO_REFERER, 127 | acceptTypes.data(), 128 | WINHTTP_FLAG_SECURE 129 | ); 130 | if (!request) { return {}; } 131 | 132 | auto apiHeader = TEXT("X-GitHub-Api-Version: 2022-11-28"); 133 | results = WinHttpSendRequest( 134 | request, 135 | apiHeader, 136 | -1L, 137 | WINHTTP_NO_REQUEST_DATA, 138 | 0, 139 | 0, 140 | 0 141 | ); 142 | if (!results) { return {}; } 143 | 144 | results = WinHttpReceiveResponse(request, nullptr); 145 | if (!results) { return {}; } 146 | 147 | DWORD dwSize = 0; 148 | std::u8string result; 149 | do { 150 | // Check for available data. 151 | dwSize = 0; 152 | if (!WinHttpQueryDataAvailable(request, &dwSize)) { return {}; } 153 | 154 | std::vector outBuffer(dwSize + 1); 155 | if (WinHttpReadData(request, outBuffer.data(), dwSize, nullptr)) { 156 | result.append(reinterpret_cast(outBuffer.data())); 157 | } else { 158 | return {}; 159 | } 160 | } while (dwSize > 0); 161 | return result; 162 | } 163 | 164 | std::string UpdateChecker::parseReleaseTag(const std::u8string& json) const { 165 | boost::system::error_code ec; 166 | value jsonValue = parse(reinterpret_cast(json.c_str()), ec); 167 | if (ec) { return {}; } 168 | 169 | auto jsonObject = jsonValue.if_object(); 170 | if (!jsonObject) { return {}; } 171 | 172 | auto tagValue = jsonObject->if_contains("tag_name"); 173 | if (!tagValue) { return {}; } 174 | 175 | auto tagString = tagValue->if_string(); 176 | if (!tagString) { return {}; } 177 | 178 | return { tagString->c_str() }; 179 | } 180 | 181 | void UpdateChecker::checkWorker() { 182 | const std::unique_lock lock(checkMutex_, std::try_to_lock); 183 | if (!lock) { return; } 184 | 185 | auto version = getVersion(); 186 | if (version.empty()) { 187 | PostMessage(mainWindow_, WM_UPDATE_CHECK, UPDATE_CHECK_ERROR, 0); 188 | return; 189 | } 190 | 191 | auto latestReleaseJson = getLatestRelease(); 192 | if (latestReleaseJson.empty()) { 193 | PostMessage(mainWindow_, WM_UPDATE_CHECK, UPDATE_CHECK_ERROR, 0); 194 | return; 195 | } 196 | 197 | auto latestReleaseTag = parseReleaseTag(latestReleaseJson); 198 | if (latestReleaseTag.empty()) { 199 | PostMessage(mainWindow_, WM_UPDATE_CHECK, UPDATE_CHECK_ERROR, 0); 200 | return; 201 | } 202 | 203 | auto isNewerResult = Util::isNewerVersion(version, latestReleaseTag); 204 | switch (isNewerResult) { 205 | case 0: 206 | PostMessage(mainWindow_, WM_UPDATE_CHECK, UPDATE_NOT_FOUND, 0); 207 | return; 208 | case 1: 209 | PostMessage(mainWindow_, WM_UPDATE_CHECK, UPDATE_FOUND, 0); 210 | return; 211 | default: 212 | PostMessage(mainWindow_, WM_UPDATE_CHECK, UPDATE_CHECK_ERROR, 0); 213 | return; 214 | break; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /SoundRemote/UpdateChecker.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | constexpr auto WM_UPDATE_CHECK = (WM_APP + 0); 9 | //wParam values for WM_UPDATE_CHECK 10 | constexpr auto UPDATE_FOUND = 0; 11 | constexpr auto UPDATE_NOT_FOUND = 1; 12 | constexpr auto UPDATE_CHECK_ERROR = 2; 13 | 14 | class UpdateChecker { 15 | public: 16 | UpdateChecker(HWND mainWindow); 17 | void checkUpdates(); 18 | 19 | private: 20 | HWND mainWindow_ = nullptr; 21 | std::mutex checkMutex_; 22 | 23 | /// 24 | /// Returns product version or an empty string on error 25 | /// 26 | /// 27 | /// Product version as string, for example "0.1.2.3" 28 | /// 29 | std::string getVersion() const; 30 | 31 | /// 32 | /// Fetches latest release JSON 33 | /// 34 | /// 35 | /// UTF-8 JSON string 36 | /// 37 | std::u8string getLatestRelease() const; 38 | 39 | /// 40 | /// Parses tag from the release JSON 41 | /// 42 | /// UTF-8 JSON string 43 | /// 44 | /// Tag name 45 | /// 46 | std::string parseReleaseTag(const std::u8string& json) const; 47 | 48 | /// 49 | /// Thread worker 50 | /// 51 | void checkWorker(); 52 | }; 53 | -------------------------------------------------------------------------------- /SoundRemote/Util.cpp: -------------------------------------------------------------------------------- 1 | #include "Util.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | void* Util::mainWindow_ = nullptr; 9 | const std::regex Util::versionRegex_{ "(\\d+)\\.(\\d+)\\.(\\d+)\\.?(\\d+)?" }; 10 | 11 | void Util::setMainWindow(void* mainWindowHWND) { 12 | mainWindow_ = mainWindowHWND; 13 | } 14 | 15 | void Util::showError(const std::string& text) { 16 | MessageBoxA(reinterpret_cast(mainWindow_), text.c_str(), "Error", MB_ICONERROR | MB_OK); 17 | } 18 | 19 | void Util::showInfo(const std::wstring& text, const std::wstring& caption) { 20 | MessageBoxW(reinterpret_cast(mainWindow_), text.c_str(), caption.c_str(), MB_ICONINFORMATION | MB_OK); 21 | } 22 | 23 | std::string Util::makeAppErrorText(const std::string& where, const std::string& what) { 24 | return where + ": " + what; 25 | } 26 | 27 | std::string Util::makeFatalErrorText(ErrorCode ec) { 28 | std::ostringstream ss; 29 | ss << "Fatal error: " << static_cast(ec); 30 | return ss.str(); 31 | } 32 | 33 | int Util::isNewerVersion(const std::string& current, const std::string& latest) { 34 | std::smatch currentMatch; 35 | if (!std::regex_search(current, currentMatch, versionRegex_)) { 36 | return 0 - 1; 37 | } 38 | std::smatch latestMatch; 39 | if (!std::regex_search(latest, latestMatch, versionRegex_)) { 40 | return 0-2; 41 | } 42 | for (int i = 1; i < currentMatch.size(); ++i) { 43 | // If the one version is shorter, i.e. 1.2.3 and 1.2.3.4 44 | if (!(currentMatch[i].matched && latestMatch[i].matched)) { 45 | if (currentMatch[i].matched) { 46 | return 0; 47 | } else { 48 | return 1; 49 | } 50 | } 51 | unsigned long curV = 0, latestV = 0; 52 | try { 53 | curV = std::stoul(currentMatch[i].str()); 54 | latestV = std::stoul(latestMatch[i].str()); 55 | } catch (...) { 56 | return -3; 57 | } 58 | if (curV < latestV) { 59 | return 1; 60 | } else if (latestV < curV) { 61 | return 0; 62 | } 63 | } 64 | if (currentMatch.suffix().length() != 0 && latestMatch.suffix().length() == 0) { 65 | return 1; 66 | } 67 | return 0; 68 | } 69 | -------------------------------------------------------------------------------- /SoundRemote/Util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | enum class ErrorCode { 7 | REMOVE_CAPTURE_PIPE_CLIENTS_LISTENER = 1, 8 | }; 9 | 10 | struct Util { 11 | enum class Endian { 12 | Big, 13 | Little 14 | }; 15 | 16 | static void setMainWindow(void* mainWindowHWND); 17 | 18 | /// 19 | /// Shows an error message box with the given text. 20 | /// 21 | /// - message to show 22 | static void showError(const std::string& text); 23 | 24 | /// 25 | /// Shows an informational message box with the given text and caption. 26 | /// 27 | /// - message to show 28 | /// - message box caption 29 | static void showInfo(const std::wstring& text, const std::wstring& caption); 30 | 31 | /// 32 | /// Makes a text describing the origin of an error and the error itself. 33 | /// 34 | /// - error origin 35 | /// - error explanation 36 | /// error message 37 | static std::string makeAppErrorText(const std::string& where, const std::string& what); 38 | 39 | /// 40 | /// Makes a text for a fatal error with ErrorCode. 41 | /// 42 | /// error code 43 | /// error message 44 | static std::string makeFatalErrorText(ErrorCode ec); 45 | 46 | /// 47 | /// Compares two versions. Versions must have at least 3 numeric parts divided by a "." 48 | /// and an optional 4th numeric part. 49 | /// If one of the versions has a text suffix, it is considered smaller: "1.0.0-beta" < "1.0.0". 50 | /// No distinction is made between text suffixes: "1.0.0-alpha03" = "1.0.0-rc01". 51 | /// 52 | /// current version 53 | /// latest release version 54 | /// 55 | /// 1 if latest is newer, 0 if not, negative number if there was error parsing versions. 56 | /// 57 | static int isNewerVersion(const std::string& current, const std::string& latest); 58 | 59 | private: 60 | static void* mainWindow_; 61 | static const std::regex versionRegex_; 62 | }; 63 | 64 | //template 65 | //struct Deleter { 66 | // void operator()(T* ptr) { 67 | // fn(ptr); 68 | // } 69 | //}; 70 | -------------------------------------------------------------------------------- /SoundRemote/framework.h: -------------------------------------------------------------------------------- 1 | // header.h : include file for standard system include files, 2 | // or project specific include files 3 | // 4 | 5 | #pragma once 6 | 7 | #include "targetver.h" 8 | 9 | #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers 10 | // Log 11 | //#include 12 | //#include 13 | //#include 14 | //#include 15 | //#include 16 | -------------------------------------------------------------------------------- /SoundRemote/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by SoundRemote.rc 4 | // 5 | #define IDC_MYICON 2 6 | #define IDD_SOUNDREMOTE_DIALOG 102 7 | #define IDS_APP_TITLE 103 8 | #define IDD_ABOUTBOX 103 9 | #define IDM_ABOUT 104 10 | #define IDS_DEFAULT_RENDER 104 11 | #define IDM_EXIT 105 12 | #define IDS_DEFAULT_CAPTURE 105 13 | #define IDS_SERVER_ADDRESSES 106 14 | #define IDI_SOUNDREMOTE 107 15 | #define IDS_CLIENTS 107 16 | #define IDS_KEYSTROKES 108 17 | #define IDC_SOUNDREMOTE 109 18 | #define IDS_MUTE 110 19 | #define IDS_UPDATE_CHECK 111 20 | #define IDS_UPDATE_FOUND 112 21 | #define IDS_UPDATE_NOT_FOUND 113 22 | #define IDS_UPDATE_CHECK_ERROR 114 23 | #define IDR_MAINFRAME 128 24 | #define IDI_SOUND_ON 131 25 | #define IDI_SOUND_OFF 132 26 | #define IDM_CHECK_UPDATES 32771 27 | #define IDM_HOMEPAGE 32772 28 | #define IDC_STATIC -1 29 | 30 | // Next default values for new objects 31 | // 32 | #ifdef APSTUDIO_INVOKED 33 | #ifndef APSTUDIO_READONLY_SYMBOLS 34 | #define _APS_NO_MFC 1 35 | #define _APS_NEXT_RESOURCE_VALUE 133 36 | #define _APS_NEXT_COMMAND_VALUE 32773 37 | #define _APS_NEXT_CONTROL_VALUE 1000 38 | #define _APS_NEXT_SYMED_VALUE 110 39 | #endif 40 | #endif 41 | -------------------------------------------------------------------------------- /SoundRemote/resources/sound_off.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoundRemote/server-windows/7bfa0dfb7785c66458f5052807fb38bc2b263fec/SoundRemote/resources/sound_off.ico -------------------------------------------------------------------------------- /SoundRemote/resources/sound_on.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoundRemote/server-windows/7bfa0dfb7785c66458f5052807fb38bc2b263fec/SoundRemote/resources/sound_on.ico -------------------------------------------------------------------------------- /SoundRemote/targetver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Including SDKDDKVer.h defines the highest available Windows platform. 4 | // If you wish to build your application for a previous Windows platform, include WinSDKVer.h and 5 | // set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. 6 | 7 | #include 8 | -------------------------------------------------------------------------------- /Tests/ClientsTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "pch.h" 8 | #include "Clients.h" 9 | 10 | namespace { 11 | using boost::asio::ip::make_address_v4; 12 | using namespace std::placeholders; 13 | 14 | class ClientsListener { 15 | public: 16 | virtual void onClientsUpdate(std::forward_list clients) = 0; 17 | }; 18 | 19 | class MockClientsListener : public ClientsListener { 20 | public: 21 | MOCK_METHOD(void, onClientsUpdate, (std::forward_list clients), (override)); 22 | }; 23 | 24 | class ClientsTest : public testing::Test { 25 | protected: 26 | void SetUp() override { 27 | clients_ = std::make_unique(); 28 | } 29 | void TearDown() override { 30 | clients_.release(); 31 | } 32 | std::unique_ptr clients_; 33 | }; 34 | 35 | TEST_F(ClientsTest, AddClientsListener) { 36 | std::vector listeners(10); 37 | for (auto&& listener : listeners) { 38 | // Expect 2 calls - the initial update and on a new client 39 | EXPECT_CALL(listener, onClientsUpdate).Times(2); 40 | } 41 | 42 | for (auto&& listener : listeners) { 43 | clients_->addClientsListener(std::bind(&MockClientsListener::onClientsUpdate, &listener, _1)); 44 | } 45 | clients_->add(make_address_v4("127.0.0.1"), Audio::Compression::none); 46 | } 47 | 48 | TEST_F(ClientsTest, Add) { 49 | const auto threadCount = 5; 50 | const auto operationsPerThread = 50; 51 | const auto operationCount = threadCount * operationsPerThread; 52 | MockClientsListener listener; 53 | // Add 1 expected call for the initial update 54 | EXPECT_CALL(listener, onClientsUpdate).Times(operationCount + 1); 55 | 56 | clients_->addClientsListener(std::bind(&MockClientsListener::onClientsUpdate, &listener, _1)); 57 | std::barrier barrier(threadCount); 58 | auto addClients = [&](int start) { 59 | barrier.arrive_and_wait(); 60 | for (int i = start; i < start + operationsPerThread; i++) { 61 | auto address = "192.168.0." + std::to_string(i); 62 | clients_->add(make_address_v4(address), Audio::Compression::none); 63 | } 64 | }; 65 | std::vector threads(threadCount); 66 | for (auto i = 0; i < operationCount; i+= operationsPerThread) { 67 | threads.emplace_back(addClients, i); 68 | } 69 | } 70 | 71 | TEST_F(ClientsTest, SetCompression) { 72 | using Audio::Compression; 73 | const auto compressions = { 74 | Compression::kbps_64, 75 | Compression::kbps_128, 76 | Compression::kbps_192, 77 | Compression::kbps_256, 78 | Compression::kbps_320 79 | }; 80 | const auto address = make_address_v4("127.0.0.1"); 81 | const auto threadCount = 5; 82 | 83 | MockClientsListener listener; 84 | // Expect the initial update 85 | EXPECT_CALL(listener, onClientsUpdate); 86 | std::forward_list clients; 87 | clients.push_front(ClientInfo(address, Compression::none)); 88 | EXPECT_CALL(listener, onClientsUpdate(clients)); 89 | for (auto&& c : compressions) { 90 | clients.front().compression = c; 91 | EXPECT_CALL(listener, onClientsUpdate(clients)); 92 | } 93 | 94 | clients_->addClientsListener(std::bind(&MockClientsListener::onClientsUpdate, &listener, _1)); 95 | clients_->add(address, Compression::none); 96 | std::barrier barrier(threadCount); 97 | auto compressionsSetter = [&](Audio::Compression compression) { 98 | barrier.arrive_and_wait(); 99 | clients_->setCompression(address, compression); 100 | }; 101 | std::vector threads(threadCount); 102 | for (auto&& c : compressions) { 103 | threads.emplace_back(compressionsSetter, c); 104 | } 105 | } 106 | 107 | TEST_F(ClientsTest, Remove) { 108 | const auto threadCount = 5; 109 | const auto operationsPerThread = 50; 110 | const auto operationCount = threadCount * operationsPerThread; 111 | MockClientsListener listener; 112 | // Add 1 expected call for the initial update 113 | EXPECT_CALL(listener, onClientsUpdate).Times(operationCount + 1); 114 | for (int i = 0; i < operationCount; i++) { 115 | auto address = "192.168.0." + std::to_string(i); 116 | clients_->add(make_address_v4(address), Audio::Compression::none); 117 | } 118 | 119 | clients_->addClientsListener(std::bind(&MockClientsListener::onClientsUpdate, &listener, _1)); 120 | std::barrier barrier(threadCount); 121 | auto removeClients = [&](int start) { 122 | barrier.arrive_and_wait(); 123 | for (int i = start; i < start + operationsPerThread; i++) { 124 | auto address = "192.168.0." + std::to_string(i); 125 | clients_->remove(make_address_v4(address)); 126 | } 127 | }; 128 | std::vector threads(threadCount); 129 | for (auto i = 0; i < operationCount; i += operationsPerThread) { 130 | threads.emplace_back(removeClients, i); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Tests/EncoderOpusTest.cpp: -------------------------------------------------------------------------------- 1 | #include "pch.h" 2 | #include "EncoderOpus.h" 3 | #include "AudioUtil.h" 4 | 5 | namespace { 6 | using namespace Audio; 7 | 8 | class SampleRates : public testing::TestWithParam { 9 | public: 10 | void SetUp() override { sampleRate_ = GetParam(); } 11 | protected: 12 | Opus::SampleRate sampleRate_ = {}; 13 | }; 14 | 15 | TEST_P(SampleRates, CreatesWithValidSampleRate) { 16 | EXPECT_NO_THROW(EncoderOpus(Compression::kbps_128, sampleRate_, Opus::Channels::mono)); 17 | EXPECT_NO_THROW(EncoderOpus(Compression::kbps_128, sampleRate_, Opus::Channels::stereo)); 18 | } 19 | 20 | INSTANTIATE_TEST_SUITE_P(EncoderOpusTest, SampleRates, ::testing::Values( 21 | Opus::SampleRate::khz_8, 22 | Opus::SampleRate::khz_12, 23 | Opus::SampleRate::khz_16, 24 | Opus::SampleRate::khz_24, 25 | Opus::SampleRate::khz_48 26 | ), [](const testing::TestParamInfo& info) { 27 | const int value = static_cast(info.param); 28 | return std::to_string(value); 29 | }); 30 | 31 | class Compressions : public testing::TestWithParam { 32 | public: 33 | void SetUp() override { compression_ = GetParam(); } 34 | protected: 35 | Compression compression_ = {}; 36 | }; 37 | 38 | TEST_P(Compressions, CreatesWithValidCompression) { 39 | EXPECT_NO_THROW(EncoderOpus(compression_, Opus::SampleRate::khz_48, Opus::Channels::mono)); 40 | EXPECT_NO_THROW(EncoderOpus(compression_, Opus::SampleRate::khz_48, Opus::Channels::stereo)); 41 | } 42 | 43 | INSTANTIATE_TEST_SUITE_P(EncoderOpusTest, Compressions, ::testing::Values( 44 | Compression::kbps_64, 45 | Compression::kbps_128, 46 | Compression::kbps_192, 47 | Compression::kbps_256, 48 | Compression::kbps_320 49 | ), [](const testing::TestParamInfo& info) { 50 | const int value = static_cast(info.param); 51 | return std::to_string(value); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /Tests/KeystrokeTest.cpp: -------------------------------------------------------------------------------- 1 | #include "pch.h" 2 | #include "keystroke.h" 3 | 4 | using ::testing::HasSubstr; 5 | 6 | namespace { 7 | constexpr int WIN = 1; 8 | constexpr int CTRL = (1 << 1); 9 | constexpr int SHIFT = (1 << 2); 10 | constexpr int ALT = (1 << 3); 11 | 12 | TEST(KeystrokeTest, ToStringNoMods) { 13 | const Keystroke keystroke(0x57, 0); 14 | const std::wstring expected = L"W"; 15 | 16 | const std::wstring actual = keystroke.toString(); 17 | 18 | EXPECT_EQ(actual, expected); 19 | } 20 | 21 | TEST(KeystrokeTest, ToStringAllMods) { 22 | const Keystroke keystroke(0x57, WIN | CTRL | SHIFT | ALT); 23 | const std::wstring expectedContain[] = {L"Win", L"Ctrl", L"Shift", L"Alt"}; 24 | 25 | const std::wstring actual = keystroke.toString(); 26 | 27 | for (auto&& modStr: expectedContain) { 28 | EXPECT_THAT(actual, HasSubstr(modStr)); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/NetUtilTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "pch.h" 7 | #include "NetUtil.h" 8 | 9 | namespace { 10 | std::vector initPacket(std::initializer_list data) { 11 | std::vector result(data.size()); 12 | for (int i = 0; i < data.size(); ++i) { 13 | result[i] = static_cast(*(data.begin() + i)); 14 | } 15 | return result; 16 | } 17 | 18 | constexpr char audioDataArr[]{ 0xFAu, 0xFBu, 0x01u, 0x12u }; 19 | constexpr std::span audioData{ audioDataArr }; 20 | } 21 | 22 | namespace { 23 | using ::testing::Combine; 24 | using ::testing::TestWithParam; 25 | using ::testing::Values; 26 | using ::testing::TestParamInfo; 27 | using namespace Net::Packet; 28 | 29 | // createAudioPacket 30 | TEST(Net, createAudioPacketUncompressed) { 31 | std::vector expectedBE = initPacket({ 32 | 0xA5, 0x71, 0x20, 0x00, 0x0D, 33 | 0x00, 0xFF, 0x00, 0x00, 34 | 0xFA, 0xFB, 0x01, 0x12}); 35 | 36 | const auto actual = Net::createAudioPacket(Category::AudioDataUncompressed, 16'711'680u, audioData); 37 | 38 | EXPECT_EQ(actual, expectedBE); 39 | } 40 | 41 | TEST(Net, createAudioPacketOpus) { 42 | std::vector expectedBE = initPacket({ 43 | 0xA5, 0x71, 0x21, 0x00, 0x0D, 44 | 0xEE, 0x6B, 0x28, 0x00, 45 | 0xFA, 0xFB, 0x01, 0x12 }); 46 | 47 | const auto actual = Net::createAudioPacket(Category::AudioDataOpus, 4'000'000'000u, audioData); 48 | 49 | EXPECT_EQ(actual, expectedBE); 50 | } 51 | 52 | // createKeepAlivePacket 53 | TEST(Net, createKeepAlivePacket) { 54 | std::vector expectedBE = initPacket({ 0xA5, 0x71, 0x31, 0 , 0x05 }); 55 | 56 | const auto actual = Net::createKeepAlivePacket(); 57 | 58 | EXPECT_EQ(actual, expectedBE); 59 | } 60 | 61 | // createDisconnectPacket 62 | TEST(Net, createDisconnectPacket) { 63 | std::vector expectedBE = initPacket({ 0xA5, 0x71, 0x02, 0, 0x05 }); 64 | 65 | const auto actual = Net::createDisconnectPacket(); 66 | 67 | EXPECT_EQ(actual, expectedBE); 68 | } 69 | 70 | // compressionFromNetworkValue 71 | using Audio::Compression; 72 | class CompressionFromNetworkValue : public TestWithParam>> { 73 | protected: 74 | void SetUp() override { 75 | std::tie(netCompression_, expected_) = GetParam(); 76 | } 77 | CompressionType netCompression_ = {}; 78 | std::optional expected_ = {}; 79 | }; 80 | 81 | TEST_P(CompressionFromNetworkValue, ReturnsCorrectCompression) { 82 | auto actual = Net::compressionFromNetworkValue(netCompression_); 83 | EXPECT_EQ(expected_, actual); 84 | } 85 | 86 | INSTANTIATE_TEST_SUITE_P(Net, CompressionFromNetworkValue, Values( 87 | std::tuple{ 0, Compression::none }, 88 | std::tuple{ 1, Compression::kbps_64 }, 89 | std::tuple{ 2, Compression::kbps_128 }, 90 | std::tuple{ 3, Compression::kbps_192 }, 91 | std::tuple{ 4, Compression::kbps_256 }, 92 | std::tuple{ 5, Compression::kbps_320 } 93 | ),[](const TestParamInfo& info) { 94 | return std::to_string(static_cast(std::get<1>(info.param).value())); 95 | } 96 | ); 97 | 98 | // createAckConnectPacket 99 | TEST(Net, createAckConnectPacket) { 100 | std::vector expectedBE = initPacket({ 101 | 0xA5, 0x71, 0xF0, 0, 0x0B, 102 | 0xDD, 0xD5, Net::protocolVersion, 0, 0, 0 }); 103 | 104 | RequestIdType requestId = 0xDDD5; 105 | const auto actual = Net::createAckConnectPacket(requestId); 106 | 107 | EXPECT_EQ(actual, expectedBE); 108 | } 109 | 110 | // createAckSetFormatPacket 111 | TEST(Net, createAckSetFormatPacket) { 112 | std::vector expectedBE = initPacket({ 113 | 0xA5, 0x71, 0xF0, 0, 0x0B, 114 | 0xF0, 0xF1, 0, 0, 0, 0 }); 115 | 116 | RequestIdType requestId = 0xF0F1; 117 | const auto actual = Net::createAckSetFormatPacket(requestId); 118 | 119 | EXPECT_EQ(actual, expectedBE); 120 | } 121 | 122 | // getPacketCategory 123 | TEST(Net, getPacketCategory) { 124 | Net::Packet::Category expected = Net::Packet::Category::Disconnect; 125 | std::array packet{ static_cast(0xA5), 0x71, 0x02, 0, 0 }; 126 | 127 | const auto actual = Net::getPacketCategory({ packet.data(), packet.size()}); 128 | 129 | EXPECT_EQ(actual, expected); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /Tests/Tests.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | {2ab4baff-28f1-4da0-91a8-4e5125d59f99} 23 | Win32Proj 24 | 10.0.22621.0 25 | Application 26 | v143 27 | Unicode 28 | Tests 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ..\SoundRemote\;$(IncludePath) 38 | $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ 39 | $(SolutionDir)$(Platform)\$(Configuration)\ 40 | 41 | 42 | ..\SoundRemote\;$(IncludePath) 43 | $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ 44 | $(SolutionDir)$(Platform)\$(Configuration)\ 45 | 46 | 47 | ..\SoundRemote\;$(IncludePath) 48 | $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ 49 | 50 | 51 | ..\SoundRemote\;$(IncludePath) 52 | $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName)\ 53 | 54 | 55 | 56 | 57 | NotUsing 58 | pch.h 59 | Disabled 60 | WIN32;WIN32_LEAN_AND_MEAN;_WIN32_WINNT=0x0601;_CONSOLE;_DEBUG;%(PreprocessorDefinitions) 61 | EnableFastChecks 62 | MultiThreadedDebugDLL 63 | Level3 64 | %BOOST_ROOT% 65 | stdcpp20 66 | 67 | 68 | true 69 | Console 70 | opus.lib;iphlpapi.lib;AudioUtil.obj;Clients.obj;EncoderOpus.obj;Keystroke.obj;NetUtil.obj;Util.obj;%(AdditionalDependencies) 71 | $(SolutionDir)lib\opus\$(Platform);$(SolutionDir)$(Platform)\$(Configuration)\SoundRemote;%(AdditionalLibraryDirectories) 72 | msvcrt.lib 73 | 74 | 75 | 76 | 77 | NotUsing 78 | pch.h 79 | Disabled 80 | X64;WIN32_LEAN_AND_MEAN;_WIN32_WINNT=0x0601;_CONSOLE;_DEBUG;%(PreprocessorDefinitions) 81 | EnableFastChecks 82 | MultiThreadedDebugDLL 83 | Level3 84 | %BOOST_ROOT% 85 | stdcpp20 86 | 87 | 88 | true 89 | Console 90 | opus.lib;iphlpapi.lib;AudioUtil.obj;Clients.obj;EncoderOpus.obj;Keystroke.obj;NetUtil.obj;Util.obj;%(AdditionalDependencies) 91 | $(SolutionDir)lib\opus\$(Platform);$(SolutionDir)$(Platform)\$(Configuration)\SoundRemote;%(AdditionalLibraryDirectories) 92 | msvcrt.lib 93 | 94 | 95 | 96 | 97 | NotUsing 98 | pch.h 99 | WIN32;WIN32_LEAN_AND_MEAN;_WIN32_WINNT=0x0601;_CONSOLE;NDEBUG;%(PreprocessorDefinitions) 100 | MultiThreadedDLL 101 | Level3 102 | ProgramDatabase 103 | %BOOST_ROOT% 104 | stdcpp20 105 | 106 | 107 | true 108 | Console 109 | true 110 | true 111 | opus.lib;iphlpapi.lib;AudioUtil.obj;Clients.obj;EncoderOpus.obj;Keystroke.obj;NetUtil.obj;Util.obj;%(AdditionalDependencies) 112 | $(SolutionDir)lib\opus\$(Platform);$(SolutionDir)$(Platform)\$(Configuration)\SoundRemote;%(AdditionalLibraryDirectories) 113 | 114 | 115 | 116 | 117 | NotUsing 118 | pch.h 119 | X64;WIN32_LEAN_AND_MEAN;_WIN32_WINNT=0x0601;_CONSOLE;NDEBUG;%(PreprocessorDefinitions) 120 | MultiThreadedDLL 121 | Level3 122 | ProgramDatabase 123 | %BOOST_ROOT% 124 | stdcpp20 125 | 126 | 127 | true 128 | Console 129 | true 130 | true 131 | opus.lib;iphlpapi.lib;AudioUtil.obj;Clients.obj;EncoderOpus.obj;Keystroke.obj;NetUtil.obj;Util.obj;%(AdditionalDependencies) 132 | $(SolutionDir)lib\opus\$(Platform);$(SolutionDir)$(Platform)\$(Configuration)\SoundRemote;%(AdditionalLibraryDirectories) 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | Create 162 | Create 163 | Create 164 | Create 165 | 166 | 167 | 168 | 169 | 170 | {d85222ef-3a69-4496-8f74-1e7e652cecff} 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /Tests/Tests.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | gtest Package Files 9 | 10 | 11 | gtest Package Files 12 | 13 | 14 | gtest Package Files 15 | 16 | 17 | 18 | Header Tests 19 | 20 | 21 | Header Tests 22 | 23 | 24 | Header Tests 25 | 26 | 27 | Header Tests 28 | 29 | 30 | Header Tests 31 | 32 | 33 | Header Tests 34 | 35 | 36 | Header Tests 37 | 38 | 39 | Header Tests 40 | 41 | 42 | Header Tests 43 | 44 | 45 | Header Tests 46 | 47 | 48 | Header Tests 49 | 50 | 51 | Header Tests 52 | 53 | 54 | Header Tests 55 | 56 | 57 | Header Tests 58 | 59 | 60 | Header Tests 61 | 62 | 63 | 64 | Header Tests 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {f58def6f-26f7-414c-a9f0-ffc861515233} 74 | 75 | 76 | {b33345fd-7398-4173-a616-d15c66103f4c} 77 | 78 | 79 | 80 | 81 | gtest Package Files 82 | 83 | 84 | -------------------------------------------------------------------------------- /Tests/UtilTest.cpp: -------------------------------------------------------------------------------- 1 | #include "pch.h" 2 | #include "Util.h" 3 | 4 | namespace { 5 | struct VersionParams { 6 | std::string current; 7 | std::string latest; 8 | int expected; 9 | }; 10 | 11 | class IsNewerVersion : public testing::TestWithParam { 12 | public: 13 | void SetUp() override { params = GetParam(); } 14 | protected: 15 | VersionParams params; 16 | }; 17 | 18 | TEST_P(IsNewerVersion, ComparesVersionsCorrectly) { 19 | const int actual = Util::isNewerVersion(params.current, params.latest); 20 | if (params.expected < 0) { 21 | // Check that actual is negative, not for an exact match to the params.expected 22 | EXPECT_TRUE(actual < 0); 23 | } else { 24 | EXPECT_EQ(params.expected, actual); 25 | } 26 | } 27 | 28 | INSTANTIATE_TEST_SUITE_P(Util, IsNewerVersion, ::testing::Values( 29 | // not newer 30 | VersionParams{ { "1.0.0.0" }, { "1.0.0.0" }, 0 }, 31 | VersionParams{ { "0.2.0.0" }, { "v0.2.0" }, 0 }, 32 | VersionParams{ { "v1.0.8.0" }, { "1.0.8.0" }, 0 }, 33 | VersionParams{ { "0.2.3.0" }, { "Prefix 0.2.3.0" }, 0 }, 34 | VersionParams{ { "1.2.3.4" }, { "1.2.3.4-rc1" }, 0 }, 35 | VersionParams{ { "1.1.0.100" }, { "1.0.99" }, 0 }, 36 | VersionParams{ { "58.2.0.0" }, { "0x58.2.0.0" }, 0 }, 37 | // newer 38 | VersionParams{ { "0.4.1.0" }, { "0.5.0" }, 1 }, 39 | VersionParams{ { "v0.0.3" }, { "0.0.3.0" }, 1 }, 40 | VersionParams{ { "1.2.3.4-alpha" }, { "1.2.3.4" }, 1 }, 41 | VersionParams{ { "1.2.3" }, { "1.2.3.0-alpha" }, 1 }, 42 | VersionParams{ { "0.0.4.9" }, { "0.0.4.10" }, 1 }, 43 | // invalid 44 | VersionParams{ { "1.2.3.4" }, { "1.2" }, -1 }, 45 | VersionParams{ { "1.2" }, { "1.2.0" }, -1 }, 46 | VersionParams{ { "0.2.0.0" }, { "1.2..0.0" }, -1 }, 47 | VersionParams{ { "1.a.0.0" }, { "1.2.0.0" }, -1 } 48 | )); 49 | } 50 | -------------------------------------------------------------------------------- /Tests/header_tests/AudioCaptureHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "AudioCapture.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, AudioCaptureCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/AudioResamplerHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "AudioResampler.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, AudioResamplerCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/AudioUtilHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "AudioUtil.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, AudioUtilCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/CapturePipeHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "CapturePipe.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, CapturePipeCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/ClientsHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "Clients.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, ClientsCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/ControlsHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "Controls.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, ControlsCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/EncoderOpusHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "EncoderOpus.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, EncoderOpusCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/KeystrokeHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "Keystroke.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, KeystrokeCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/NetDefinesHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "NetDefines.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, NetDefinesCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/NetUtilHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "NetUtil.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, NetUtilCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/ServerHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "Server.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, ServerCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/SettingsHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "Settings.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, SettingsCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/SettingsImplHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "SettingsImpl.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, SettingsImplCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/SoundRemoteAppHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "SoundRemoteApp.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, SoundRemoteAppCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/UpdateCheckerHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "UpdateChecker.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, UpdateCheckerCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/header_tests/UtilHTest.cpp: -------------------------------------------------------------------------------- 1 | #include "../pch.h" 2 | #include "Util.h" 3 | 4 | namespace { 5 | TEST(HeaderTest, UtilCompiles) { 6 | EXPECT_TRUE(true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /Tests/pch.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // pch.cpp 3 | // 4 | 5 | #include "pch.h" 6 | -------------------------------------------------------------------------------- /Tests/pch.h: -------------------------------------------------------------------------------- 1 | // 2 | // pch.h 3 | // 4 | 5 | #pragma once 6 | 7 | #include "gtest/gtest.h" 8 | #include "gmock/gmock.h" 9 | -------------------------------------------------------------------------------- /include/opus/meson.build: -------------------------------------------------------------------------------- 1 | opus_headers = [ 2 | 'opus.h', 3 | 'opus_multistream.h', 4 | 'opus_projection.h', 5 | 'opus_types.h', 6 | 'opus_defines.h', 7 | ] 8 | 9 | if opt_custom_modes 10 | opus_headers += ['opus_custom.h'] 11 | endif 12 | 13 | install_headers(opus_headers, subdir: 'opus') 14 | -------------------------------------------------------------------------------- /include/opus/opus_custom.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2007-2008 CSIRO 2 | Copyright (c) 2007-2009 Xiph.Org Foundation 3 | Copyright (c) 2008-2012 Gregory Maxwell 4 | Written by Jean-Marc Valin and Gregory Maxwell */ 5 | /* 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | - Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | - Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 21 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | */ 29 | 30 | /** 31 | @file opus_custom.h 32 | @brief Opus-Custom reference implementation API 33 | */ 34 | 35 | #ifndef OPUS_CUSTOM_H 36 | #define OPUS_CUSTOM_H 37 | 38 | #include "opus_defines.h" 39 | 40 | #ifdef __cplusplus 41 | extern "C" { 42 | #endif 43 | 44 | #ifdef CUSTOM_MODES 45 | # define OPUS_CUSTOM_EXPORT OPUS_EXPORT 46 | # define OPUS_CUSTOM_EXPORT_STATIC OPUS_EXPORT 47 | #else 48 | # define OPUS_CUSTOM_EXPORT 49 | # ifdef OPUS_BUILD 50 | # define OPUS_CUSTOM_EXPORT_STATIC static OPUS_INLINE 51 | # else 52 | # define OPUS_CUSTOM_EXPORT_STATIC 53 | # endif 54 | #endif 55 | 56 | /** @defgroup opus_custom Opus Custom 57 | * @{ 58 | * Opus Custom is an optional part of the Opus specification and 59 | * reference implementation which uses a distinct API from the regular 60 | * API and supports frame sizes that are not normally supported.\ Use 61 | * of Opus Custom is discouraged for all but very special applications 62 | * for which a frame size different from 2.5, 5, 10, or 20 ms is needed 63 | * (for either complexity or latency reasons) and where interoperability 64 | * is less important. 65 | * 66 | * In addition to the interoperability limitations the use of Opus custom 67 | * disables a substantial chunk of the codec and generally lowers the 68 | * quality available at a given bitrate. Normally when an application needs 69 | * a different frame size from the codec it should buffer to match the 70 | * sizes but this adds a small amount of delay which may be important 71 | * in some very low latency applications. Some transports (especially 72 | * constant rate RF transports) may also work best with frames of 73 | * particular durations. 74 | * 75 | * Libopus only supports custom modes if they are enabled at compile time. 76 | * 77 | * The Opus Custom API is similar to the regular API but the 78 | * @ref opus_encoder_create and @ref opus_decoder_create calls take 79 | * an additional mode parameter which is a structure produced by 80 | * a call to @ref opus_custom_mode_create. Both the encoder and decoder 81 | * must create a mode using the same sample rate (fs) and frame size 82 | * (frame size) so these parameters must either be signaled out of band 83 | * or fixed in a particular implementation. 84 | * 85 | * Similar to regular Opus the custom modes support on the fly frame size 86 | * switching, but the sizes available depend on the particular frame size in 87 | * use. For some initial frame sizes on a single on the fly size is available. 88 | */ 89 | 90 | /** Contains the state of an encoder. One encoder state is needed 91 | for each stream. It is initialized once at the beginning of the 92 | stream. Do *not* re-initialize the state for every frame. 93 | @brief Encoder state 94 | */ 95 | typedef struct OpusCustomEncoder OpusCustomEncoder; 96 | 97 | /** State of the decoder. One decoder state is needed for each stream. 98 | It is initialized once at the beginning of the stream. Do *not* 99 | re-initialize the state for every frame. 100 | @brief Decoder state 101 | */ 102 | typedef struct OpusCustomDecoder OpusCustomDecoder; 103 | 104 | /** The mode contains all the information necessary to create an 105 | encoder. Both the encoder and decoder need to be initialized 106 | with exactly the same mode, otherwise the output will be 107 | corrupted. The mode MUST NOT BE DESTROYED until the encoders and 108 | decoders that use it are destroyed as well. 109 | @brief Mode configuration 110 | */ 111 | typedef struct OpusCustomMode OpusCustomMode; 112 | 113 | /** Creates a new mode struct. This will be passed to an encoder or 114 | * decoder. The mode MUST NOT BE DESTROYED until the encoders and 115 | * decoders that use it are destroyed as well. 116 | * @param [in] Fs int: Sampling rate (8000 to 96000 Hz) 117 | * @param [in] frame_size int: Number of samples (per channel) to encode in each 118 | * packet (64 - 1024, prime factorization must contain zero or more 2s, 3s, or 5s and no other primes) 119 | * @param [out] error int*: Returned error code (if NULL, no error will be returned) 120 | * @return A newly created mode 121 | */ 122 | OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomMode *opus_custom_mode_create(opus_int32 Fs, int frame_size, int *error); 123 | 124 | /** Destroys a mode struct. Only call this after all encoders and 125 | * decoders using this mode are destroyed as well. 126 | * @param [in] mode OpusCustomMode*: Mode to be freed. 127 | */ 128 | OPUS_CUSTOM_EXPORT void opus_custom_mode_destroy(OpusCustomMode *mode); 129 | 130 | 131 | #if !defined(OPUS_BUILD) || defined(CELT_ENCODER_C) 132 | 133 | /* Encoder */ 134 | /** Gets the size of an OpusCustomEncoder structure. 135 | * @param [in] mode OpusCustomMode *: Mode configuration 136 | * @param [in] channels int: Number of channels 137 | * @returns size 138 | */ 139 | OPUS_CUSTOM_EXPORT_STATIC OPUS_WARN_UNUSED_RESULT int opus_custom_encoder_get_size( 140 | const OpusCustomMode *mode, 141 | int channels 142 | ) OPUS_ARG_NONNULL(1); 143 | 144 | # ifdef CUSTOM_MODES 145 | /** Initializes a previously allocated encoder state 146 | * The memory pointed to by st must be the size returned by opus_custom_encoder_get_size. 147 | * This is intended for applications which use their own allocator instead of malloc. 148 | * @see opus_custom_encoder_create(),opus_custom_encoder_get_size() 149 | * To reset a previously initialized state use the OPUS_RESET_STATE CTL. 150 | * @param [in] st OpusCustomEncoder*: Encoder state 151 | * @param [in] mode OpusCustomMode *: Contains all the information about the characteristics of 152 | * the stream (must be the same characteristics as used for the 153 | * decoder) 154 | * @param [in] channels int: Number of channels 155 | * @return OPUS_OK Success or @ref opus_errorcodes 156 | */ 157 | OPUS_CUSTOM_EXPORT int opus_custom_encoder_init( 158 | OpusCustomEncoder *st, 159 | const OpusCustomMode *mode, 160 | int channels 161 | ) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); 162 | # endif 163 | #endif 164 | 165 | 166 | /** Creates a new encoder state. Each stream needs its own encoder 167 | * state (can't be shared across simultaneous streams). 168 | * @param [in] mode OpusCustomMode*: Contains all the information about the characteristics of 169 | * the stream (must be the same characteristics as used for the 170 | * decoder) 171 | * @param [in] channels int: Number of channels 172 | * @param [out] error int*: Returns an error code 173 | * @return Newly created encoder state. 174 | */ 175 | OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomEncoder *opus_custom_encoder_create( 176 | const OpusCustomMode *mode, 177 | int channels, 178 | int *error 179 | ) OPUS_ARG_NONNULL(1); 180 | 181 | 182 | /** Destroys an encoder state. 183 | * @param[in] st OpusCustomEncoder*: State to be freed. 184 | */ 185 | OPUS_CUSTOM_EXPORT void opus_custom_encoder_destroy(OpusCustomEncoder *st); 186 | 187 | /** Encodes a frame of audio. 188 | * @param [in] st OpusCustomEncoder*: Encoder state 189 | * @param [in] pcm float*: PCM audio in float format, with a normal range of +/-1.0. 190 | * Samples with a range beyond +/-1.0 are supported but will 191 | * be clipped by decoders using the integer API and should 192 | * only be used if it is known that the far end supports 193 | * extended dynamic range. There must be exactly 194 | * frame_size samples per channel. 195 | * @param [in] frame_size int: Number of samples per frame of input signal 196 | * @param [out] compressed char *: The compressed data is written here. This may not alias pcm and must be at least maxCompressedBytes long. 197 | * @param [in] maxCompressedBytes int: Maximum number of bytes to use for compressing the frame 198 | * (can change from one frame to another) 199 | * @return Number of bytes written to "compressed". 200 | * If negative, an error has occurred (see error codes). It is IMPORTANT that 201 | * the length returned be somehow transmitted to the decoder. Otherwise, no 202 | * decoding is possible. 203 | */ 204 | OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_encode_float( 205 | OpusCustomEncoder *st, 206 | const float *pcm, 207 | int frame_size, 208 | unsigned char *compressed, 209 | int maxCompressedBytes 210 | ) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); 211 | 212 | /** Encodes a frame of audio. 213 | * @param [in] st OpusCustomEncoder*: Encoder state 214 | * @param [in] pcm opus_int16*: PCM audio in signed 16-bit format (native endian). 215 | * There must be exactly frame_size samples per channel. 216 | * @param [in] frame_size int: Number of samples per frame of input signal 217 | * @param [out] compressed char *: The compressed data is written here. This may not alias pcm and must be at least maxCompressedBytes long. 218 | * @param [in] maxCompressedBytes int: Maximum number of bytes to use for compressing the frame 219 | * (can change from one frame to another) 220 | * @return Number of bytes written to "compressed". 221 | * If negative, an error has occurred (see error codes). It is IMPORTANT that 222 | * the length returned be somehow transmitted to the decoder. Otherwise, no 223 | * decoding is possible. 224 | */ 225 | OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_encode( 226 | OpusCustomEncoder *st, 227 | const opus_int16 *pcm, 228 | int frame_size, 229 | unsigned char *compressed, 230 | int maxCompressedBytes 231 | ) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); 232 | 233 | /** Perform a CTL function on an Opus custom encoder. 234 | * 235 | * Generally the request and subsequent arguments are generated 236 | * by a convenience macro. 237 | * @see opus_encoderctls 238 | */ 239 | OPUS_CUSTOM_EXPORT int opus_custom_encoder_ctl(OpusCustomEncoder * OPUS_RESTRICT st, int request, ...) OPUS_ARG_NONNULL(1); 240 | 241 | 242 | #if !defined(OPUS_BUILD) || defined(CELT_DECODER_C) 243 | /* Decoder */ 244 | 245 | /** Gets the size of an OpusCustomDecoder structure. 246 | * @param [in] mode OpusCustomMode *: Mode configuration 247 | * @param [in] channels int: Number of channels 248 | * @returns size 249 | */ 250 | OPUS_CUSTOM_EXPORT_STATIC OPUS_WARN_UNUSED_RESULT int opus_custom_decoder_get_size( 251 | const OpusCustomMode *mode, 252 | int channels 253 | ) OPUS_ARG_NONNULL(1); 254 | 255 | /** Initializes a previously allocated decoder state 256 | * The memory pointed to by st must be the size returned by opus_custom_decoder_get_size. 257 | * This is intended for applications which use their own allocator instead of malloc. 258 | * @see opus_custom_decoder_create(),opus_custom_decoder_get_size() 259 | * To reset a previously initialized state use the OPUS_RESET_STATE CTL. 260 | * @param [in] st OpusCustomDecoder*: Decoder state 261 | * @param [in] mode OpusCustomMode *: Contains all the information about the characteristics of 262 | * the stream (must be the same characteristics as used for the 263 | * encoder) 264 | * @param [in] channels int: Number of channels 265 | * @return OPUS_OK Success or @ref opus_errorcodes 266 | */ 267 | OPUS_CUSTOM_EXPORT_STATIC int opus_custom_decoder_init( 268 | OpusCustomDecoder *st, 269 | const OpusCustomMode *mode, 270 | int channels 271 | ) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2); 272 | 273 | #endif 274 | 275 | 276 | /** Creates a new decoder state. Each stream needs its own decoder state (can't 277 | * be shared across simultaneous streams). 278 | * @param [in] mode OpusCustomMode: Contains all the information about the characteristics of the 279 | * stream (must be the same characteristics as used for the encoder) 280 | * @param [in] channels int: Number of channels 281 | * @param [out] error int*: Returns an error code 282 | * @return Newly created decoder state. 283 | */ 284 | OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomDecoder *opus_custom_decoder_create( 285 | const OpusCustomMode *mode, 286 | int channels, 287 | int *error 288 | ) OPUS_ARG_NONNULL(1); 289 | 290 | /** Destroys a decoder state. 291 | * @param[in] st OpusCustomDecoder*: State to be freed. 292 | */ 293 | OPUS_CUSTOM_EXPORT void opus_custom_decoder_destroy(OpusCustomDecoder *st); 294 | 295 | /** Decode an opus custom frame with floating point output 296 | * @param [in] st OpusCustomDecoder*: Decoder state 297 | * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss 298 | * @param [in] len int: Number of bytes in payload 299 | * @param [out] pcm float*: Output signal (interleaved if 2 channels). length 300 | * is frame_size*channels*sizeof(float) 301 | * @param [in] frame_size Number of samples per channel of available space in *pcm. 302 | * @returns Number of decoded samples or @ref opus_errorcodes 303 | */ 304 | OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_decode_float( 305 | OpusCustomDecoder *st, 306 | const unsigned char *data, 307 | int len, 308 | float *pcm, 309 | int frame_size 310 | ) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); 311 | 312 | /** Decode an opus custom frame 313 | * @param [in] st OpusCustomDecoder*: Decoder state 314 | * @param [in] data char*: Input payload. Use a NULL pointer to indicate packet loss 315 | * @param [in] len int: Number of bytes in payload 316 | * @param [out] pcm opus_int16*: Output signal (interleaved if 2 channels). length 317 | * is frame_size*channels*sizeof(opus_int16) 318 | * @param [in] frame_size Number of samples per channel of available space in *pcm. 319 | * @returns Number of decoded samples or @ref opus_errorcodes 320 | */ 321 | OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_decode( 322 | OpusCustomDecoder *st, 323 | const unsigned char *data, 324 | int len, 325 | opus_int16 *pcm, 326 | int frame_size 327 | ) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); 328 | 329 | /** Perform a CTL function on an Opus custom decoder. 330 | * 331 | * Generally the request and subsequent arguments are generated 332 | * by a convenience macro. 333 | * @see opus_genericctls 334 | */ 335 | OPUS_CUSTOM_EXPORT int opus_custom_decoder_ctl(OpusCustomDecoder * OPUS_RESTRICT st, int request, ...) OPUS_ARG_NONNULL(1); 336 | 337 | /**@}*/ 338 | 339 | #ifdef __cplusplus 340 | } 341 | #endif 342 | 343 | #endif /* OPUS_CUSTOM_H */ 344 | -------------------------------------------------------------------------------- /include/opus/opus_types.h: -------------------------------------------------------------------------------- 1 | /* (C) COPYRIGHT 1994-2002 Xiph.Org Foundation */ 2 | /* Modified by Jean-Marc Valin */ 3 | /* 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | - Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | - Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 19 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 20 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 21 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 22 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 24 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | /* opus_types.h based on ogg_types.h from libogg */ 28 | 29 | /** 30 | @file opus_types.h 31 | @brief Opus reference implementation types 32 | */ 33 | #ifndef OPUS_TYPES_H 34 | #define OPUS_TYPES_H 35 | 36 | #define opus_int int /* used for counters etc; at least 16 bits */ 37 | #define opus_int64 long long 38 | #define opus_int8 signed char 39 | 40 | #define opus_uint unsigned int /* used for counters etc; at least 16 bits */ 41 | #define opus_uint64 unsigned long long 42 | #define opus_uint8 unsigned char 43 | 44 | /* Use the real stdint.h if it's there (taken from Paul Hsieh's pstdint.h) */ 45 | #if (defined(__STDC__) && __STDC__ && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || (defined(__GNUC__) && (defined(_STDINT_H) || defined(_STDINT_H_)) || defined (HAVE_STDINT_H)) 46 | #include 47 | # undef opus_int64 48 | # undef opus_int8 49 | # undef opus_uint64 50 | # undef opus_uint8 51 | typedef int8_t opus_int8; 52 | typedef uint8_t opus_uint8; 53 | typedef int16_t opus_int16; 54 | typedef uint16_t opus_uint16; 55 | typedef int32_t opus_int32; 56 | typedef uint32_t opus_uint32; 57 | typedef int64_t opus_int64; 58 | typedef uint64_t opus_uint64; 59 | #elif defined(_WIN32) 60 | 61 | # if defined(__CYGWIN__) 62 | # include <_G_config.h> 63 | typedef _G_int32_t opus_int32; 64 | typedef _G_uint32_t opus_uint32; 65 | typedef _G_int16 opus_int16; 66 | typedef _G_uint16 opus_uint16; 67 | # elif defined(__MINGW32__) 68 | typedef short opus_int16; 69 | typedef unsigned short opus_uint16; 70 | typedef int opus_int32; 71 | typedef unsigned int opus_uint32; 72 | # elif defined(__MWERKS__) 73 | typedef int opus_int32; 74 | typedef unsigned int opus_uint32; 75 | typedef short opus_int16; 76 | typedef unsigned short opus_uint16; 77 | # else 78 | /* MSVC/Borland */ 79 | typedef __int32 opus_int32; 80 | typedef unsigned __int32 opus_uint32; 81 | typedef __int16 opus_int16; 82 | typedef unsigned __int16 opus_uint16; 83 | # endif 84 | 85 | #elif defined(__MACOS__) 86 | 87 | # include 88 | typedef SInt16 opus_int16; 89 | typedef UInt16 opus_uint16; 90 | typedef SInt32 opus_int32; 91 | typedef UInt32 opus_uint32; 92 | 93 | #elif (defined(__APPLE__) && defined(__MACH__)) /* MacOS X Framework build */ 94 | 95 | # include 96 | typedef int16_t opus_int16; 97 | typedef u_int16_t opus_uint16; 98 | typedef int32_t opus_int32; 99 | typedef u_int32_t opus_uint32; 100 | 101 | #elif defined(__BEOS__) 102 | 103 | /* Be */ 104 | # include 105 | typedef int16 opus_int16; 106 | typedef u_int16 opus_uint16; 107 | typedef int32_t opus_int32; 108 | typedef u_int32_t opus_uint32; 109 | 110 | #elif defined (__EMX__) 111 | 112 | /* OS/2 GCC */ 113 | typedef short opus_int16; 114 | typedef unsigned short opus_uint16; 115 | typedef int opus_int32; 116 | typedef unsigned int opus_uint32; 117 | 118 | #elif defined (DJGPP) 119 | 120 | /* DJGPP */ 121 | typedef short opus_int16; 122 | typedef unsigned short opus_uint16; 123 | typedef int opus_int32; 124 | typedef unsigned int opus_uint32; 125 | 126 | #elif defined(R5900) 127 | 128 | /* PS2 EE */ 129 | typedef int opus_int32; 130 | typedef unsigned opus_uint32; 131 | typedef short opus_int16; 132 | typedef unsigned short opus_uint16; 133 | 134 | #elif defined(__SYMBIAN32__) 135 | 136 | /* Symbian GCC */ 137 | typedef signed short opus_int16; 138 | typedef unsigned short opus_uint16; 139 | typedef signed int opus_int32; 140 | typedef unsigned int opus_uint32; 141 | 142 | #elif defined(CONFIG_TI_C54X) || defined (CONFIG_TI_C55X) 143 | 144 | typedef short opus_int16; 145 | typedef unsigned short opus_uint16; 146 | typedef long opus_int32; 147 | typedef unsigned long opus_uint32; 148 | 149 | #elif defined(CONFIG_TI_C6X) 150 | 151 | typedef short opus_int16; 152 | typedef unsigned short opus_uint16; 153 | typedef int opus_int32; 154 | typedef unsigned int opus_uint32; 155 | 156 | #else 157 | 158 | /* Give up, take a reasonable guess */ 159 | typedef short opus_int16; 160 | typedef unsigned short opus_uint16; 161 | typedef int opus_int32; 162 | typedef unsigned int opus_uint32; 163 | 164 | #endif 165 | 166 | #endif /* OPUS_TYPES_H */ 167 | -------------------------------------------------------------------------------- /opus_license.txt: -------------------------------------------------------------------------------- 1 | Opus - https://opus-codec.org 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | - Neither the name of Internet Society, IETF or IETF Trust, nor the names of specific contributors, may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | --------------------------------------------------------------------------------