├── .gitattributes
├── .github
└── workflows
│ ├── deploy-docs.yml
│ └── dotnet-desktop.yml
├── .gitignore
├── LICENSE
├── QPlayer.sln
├── QPlayer
├── App.xaml
├── App.xaml.cs
├── AssemblyInfo.cs
├── Audio
│ ├── AudioPlaybackManager.cs
│ ├── EQSampleProvider.cs
│ ├── FadingSampleProvider.cs
│ ├── LoopingSampleProvider.cs
│ ├── MediaCacheManager.cs
│ └── VectorExtensions.cs
├── FodyWeavers.xml
├── FodyWeavers.xsd
├── Models
│ ├── OSCDriver.cs
│ ├── PeakFile.cs
│ └── ShowFile.cs
├── Properties
│ └── PublishProfiles
│ │ ├── ClickOnceProfile.pubxml
│ │ └── FolderProfile.pubxml
├── QPlayer.csproj
├── Resources
│ ├── Icon.ico
│ ├── Icon.psd
│ ├── IconL.png
│ ├── IconM.png
│ ├── IconS.png
│ ├── IconXL.png
│ ├── IconXS.png
│ ├── IconXXS.png
│ ├── IconXXXS.png
│ ├── Icons
│ │ ├── .gitignore
│ │ ├── ConvertedIcons.xaml
│ │ ├── Update.ps1
│ │ ├── arrow-right-long-solid.svg
│ │ ├── circle-solid.svg
│ │ ├── clock-solid.svg
│ │ ├── folder-open-solid.svg
│ │ ├── folder-solid.svg
│ │ ├── gear-solid.svg
│ │ ├── minus-solid.svg
│ │ ├── music-solid.svg
│ │ ├── pause-solid.svg
│ │ ├── play-solid.svg
│ │ ├── plus-solid.svg
│ │ ├── rotate-right-solid.svg
│ │ ├── rotate-solid.svg
│ │ ├── sort-solid.svg
│ │ ├── stop-solid.svg
│ │ └── volume-low-solid.svg
│ ├── Logo.ai
│ ├── Logo.svg
│ ├── LogoSmall.svg
│ ├── Splash.png
│ └── Splash.psd
├── ThemesV2
│ ├── ControlColours.xaml
│ ├── Controls.xaml
│ ├── Controls.xaml.cs
│ ├── CustomControls.xaml
│ ├── DeepDark.xaml
│ ├── Icons.xaml
│ ├── RedBlackTheme.xaml
│ ├── SoftDark.xaml
│ ├── ThemeTypes.cs
│ ├── ThemesController.cs
│ └── VeryDarkTheme.xaml
├── ViewModels
│ ├── CueViewModel.cs
│ ├── DummyCueViewModel.cs
│ ├── ExtensionMethods.cs
│ ├── GroupCueViewModel.cs
│ ├── MainViewModel.cs
│ ├── ProjectSettingsViewModel.cs
│ ├── SoundCueViewModel.cs
│ ├── StopCueViewModel.cs
│ ├── TimeCodeCueViewModel.cs
│ ├── VolumeCueViewModel.cs
│ └── WaveFormRenderer.cs
└── Views
│ ├── AboutWindow.xaml
│ ├── AboutWindow.xaml.cs
│ ├── ActiveCueControl.xaml
│ ├── ActiveCueControl.xaml.cs
│ ├── CueDataControl.xaml
│ ├── CueDataControl.xaml.cs
│ ├── CueDataHeaderControl.xaml
│ ├── CueDataHeaderControl.xaml.cs
│ ├── CueDataTemplates.xaml
│ ├── CueDataTemplates.xaml.cs
│ ├── CueEditor.xaml
│ ├── CueEditor.xaml.cs
│ ├── HiddenTextbox.xaml
│ ├── HiddenTextbox.xaml.cs
│ ├── LibraryImports.cs
│ ├── LogWindow.xaml
│ ├── LogWindow.xaml.cs
│ ├── MainWindow.xaml
│ ├── MainWindow.xaml.cs
│ ├── SettingsWindow.xaml
│ ├── SettingsWindow.xaml.cs
│ ├── TextField.xaml
│ ├── TextField.xaml.cs
│ ├── WaveForm.xaml
│ ├── WaveForm.xaml.cs
│ ├── WaveFormWindow.xaml
│ └── WaveFormWindow.xaml.cs
├── README.md
└── docs
├── .gitignore
├── .vscode
├── extensions.json
└── launch.json
├── QPlayerDocs.esproj
├── README.md
├── astro.config.mjs
├── package-lock.json
├── package.json
├── public
├── favicon.png
└── favicon.svg
├── src
├── assets
│ ├── IconXL.png
│ ├── Logo.svg
│ ├── Splash.png
│ ├── active-cue.png
│ ├── base-cue.png
│ ├── context-menu.png
│ ├── cue-playing.png
│ ├── double-click-edit.png
│ ├── drag-drop.png
│ ├── dummy-cue.png
│ ├── hints.png
│ ├── project-setup.png
│ ├── qplayer-main-window.png
│ ├── sidebar.png
│ ├── sound-cue.png
│ ├── stop-cue.png
│ ├── timecode-cue.png
│ ├── volume-cue.png
│ └── waveform-viewer.png
├── content.config.ts
├── content
│ └── docs
│ │ ├── guides
│ │ └── getting-started.mdx
│ │ ├── index.mdx
│ │ └── reference
│ │ ├── cue-stack.mdx
│ │ ├── cues
│ │ ├── _meta.yml
│ │ ├── cue.md
│ │ ├── dummy-cue.md
│ │ ├── sound-cue.md
│ │ ├── stop-cue.md
│ │ ├── timecode-cue.md
│ │ ├── video-cue.md
│ │ └── volume-cue.md
│ │ ├── index.mdx
│ │ ├── osc.md
│ │ └── project-setup.md
└── styles
│ └── custom.css
└── tsconfig.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Docs to GitHub Pages
2 |
3 | on:
4 | # Trigger the workflow every time you push to the `main` branch
5 | # Using a different branch name? Replace `main` with your branch’s name
6 | push:
7 | branches: [main]
8 | # Allows you to run this workflow manually from the Actions tab on GitHub.
9 | workflow_dispatch:
10 |
11 | # Allow this job to clone the repo and create a page deployment
12 | permissions:
13 | contents: read
14 | pages: write
15 | id-token: write
16 |
17 | # Allow one concurrent deployment
18 | concurrency:
19 | group: "pages"
20 | cancel-in-progress: true
21 |
22 | jobs:
23 | build:
24 | runs-on: ubuntu-latest
25 | steps:
26 | - name: Checkout your repository using git
27 | uses: actions/checkout@v4
28 | - name: Install, build, and upload your site output
29 | uses: withastro/action@v4
30 | with:
31 | path: /home/runner/work/QPlayer/QPlayer/docs
32 | # path: . # The root location of your Astro project inside the repository. (optional)
33 | # node-version: 16 # The specific version of Node that should be used to build your site. Defaults to 16. (optional)
34 | # package-manager: yarn # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional)
35 |
36 | deploy:
37 | needs: build
38 | runs-on: ubuntu-latest
39 | environment:
40 | name: github-pages
41 | url: ${{ steps.deployment.outputs.page_url }}
42 | steps:
43 | - name: Deploy to GitHub Pages
44 | id: deployment
45 | uses: actions/deploy-pages@v4
46 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet-desktop.yml:
--------------------------------------------------------------------------------
1 | name: Build .NET Application
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 |
11 | build:
12 |
13 | strategy:
14 | matrix:
15 | configuration: [Release]
16 |
17 | runs-on: windows-latest # For a list of available runner types, refer to
18 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
19 |
20 | env:
21 | Solution_Name: QPlayer # Replace with your solution name, i.e. MyWpfApp.sln.
22 | Project_Directory: QPlayer # Replace with the Wap project directory relative to the solution, i.e. MyWpfApp.Package.
23 | Project_Path: QPlayer/QPlayer.csproj # Replace with the path to your Wap project, i.e. MyWpf.App.Package\MyWpfApp.Package.wapproj.
24 |
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v3
28 | with:
29 | fetch-depth: 0
30 |
31 | # Install the .NET Core workload
32 | - name: Install .NET Core
33 | uses: actions/setup-dotnet@v3
34 | with:
35 | dotnet-version: 6.0.x
36 |
37 | # Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild
38 | - name: Setup MSBuild.exe
39 | uses: microsoft/setup-msbuild@v1.0.2
40 |
41 | # Execute all unit tests in the solution
42 | # - name: Execute unit tests
43 | # run: dotnet test
44 |
45 | # Restore the application to populate the obj folder with RuntimeIdentifiers
46 | - name: Restore the application
47 | run: msbuild $env:Solution_Name /t:Restore /p:Configuration=$env:Configuration
48 | env:
49 | Configuration: ${{ matrix.configuration }}
50 |
51 | # Create the app package by building and packaging the Windows Application Packaging project
52 | - name: Create the app package
53 | run: msbuild $env:Project_Path /p:Configuration=$env:Configuration /p:PublishProfile=$env:PublishProfileDir /p:PublishDir="publish/" /target:publish
54 | env:
55 | PublishProfileDir: "/QPlayer/Properties/PublishProfiles/ClickOnceProfile.pubxml"
56 | Configuration: ${{ matrix.configuration }}
57 |
58 | # Sign the assembly
59 | - name: Sign the assembly
60 | uses: GabrielAcostaEngler/signtool-code-sign@main
61 | with:
62 | certificate: '${{ secrets.Base64_Encoded_Pfx }}'
63 | cert-password: '${{ secrets.Pfx_Key }}'
64 | cert-sha1: '${{ secrets.PFX_HASH }}'
65 | folder: '${{ env.Project_Directory }}\publish'
66 | timestamp-server: 'http://timestamp.digicert.com'
67 | recursive: true
68 |
69 | # Compress build artifacts
70 | - name: Compress build artifacts
71 | run: Compress-Archive $env:Project_Directory\publish\* -DestinationPath $env:Project_Directory\QPlayer-release.zip
72 |
73 | # Upload the build artifacts
74 | - name: Upload build artifacts
75 | uses: actions/upload-artifact@v3
76 | with:
77 | name: ReleaseBinaries
78 | path: ${{ env.Project_Directory }}\QPlayer-release.zip
79 |
80 | create-release:
81 | needs: build
82 | name: "Create Release"
83 | runs-on: "ubuntu-latest"
84 | permissions:
85 | contents: write
86 |
87 | steps:
88 | - name: Download release binaries
89 | uses: actions/download-artifact@v3
90 | with:
91 | name: ReleaseBinaries
92 | - uses: "marvinpinto/action-automatic-releases@latest"
93 | with:
94 | repo_token: "${{ secrets.GITHUB_TOKEN }}"
95 | automatic_release_tag: "latest"
96 | prerelease: true
97 | title: "Development Build"
98 | files: QPlayer-release.zip
99 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vs
2 | */bin
3 | */obj
4 | *.snk
5 | *.user
6 | *.pfx
7 | SigningCertificate_Encoded.txt
8 | **/publish
9 | *.zip
10 |
--------------------------------------------------------------------------------
/QPlayer.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.5.33516.290
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QPlayer", "QPlayer\QPlayer.csproj", "{AC3A2BB3-F46C-45D0-986F-D28AC74A8649}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F279A0DB-B4EE-4EE7-A59B-C7A488E6B1C9}"
9 | ProjectSection(SolutionItems) = preProject
10 | .gitignore = .gitignore
11 | README.md = README.md
12 | EndProjectSection
13 | EndProject
14 | Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "QPlayerDocs", "docs\QPlayerDocs.esproj", "{1FFE1363-C8AC-4EE3-B83F-260ADB517F49}"
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Debug|ARM = Debug|ARM
20 | Debug|ARM64 = Debug|ARM64
21 | Debug|x64 = Debug|x64
22 | Debug|x86 = Debug|x86
23 | Release|Any CPU = Release|Any CPU
24 | Release|ARM = Release|ARM
25 | Release|ARM64 = Release|ARM64
26 | Release|x64 = Release|x64
27 | Release|x86 = Release|x86
28 | EndGlobalSection
29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
30 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Debug|ARM.ActiveCfg = Debug|Any CPU
33 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Debug|ARM.Build.0 = Debug|Any CPU
34 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Debug|ARM64.ActiveCfg = Debug|Any CPU
35 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Debug|ARM64.Build.0 = Debug|Any CPU
36 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Debug|x64.ActiveCfg = Debug|Any CPU
37 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Debug|x64.Build.0 = Debug|Any CPU
38 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Debug|x86.ActiveCfg = Debug|Any CPU
39 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Debug|x86.Build.0 = Debug|Any CPU
40 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Release|ARM.ActiveCfg = Release|Any CPU
43 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Release|ARM.Build.0 = Release|Any CPU
44 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Release|ARM64.ActiveCfg = Release|Any CPU
45 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Release|ARM64.Build.0 = Release|Any CPU
46 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Release|x64.ActiveCfg = Release|x64
47 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Release|x64.Build.0 = Release|x64
48 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Release|x86.ActiveCfg = Release|x86
49 | {AC3A2BB3-F46C-45D0-986F-D28AC74A8649}.Release|x86.Build.0 = Release|x86
50 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|Any CPU.Build.0 = Debug|Any CPU
52 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
53 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|ARM.ActiveCfg = Debug|Any CPU
54 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|ARM.Build.0 = Debug|Any CPU
55 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|ARM.Deploy.0 = Debug|Any CPU
56 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|ARM64.ActiveCfg = Debug|Any CPU
57 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|ARM64.Build.0 = Debug|Any CPU
58 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|ARM64.Deploy.0 = Debug|Any CPU
59 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|x64.ActiveCfg = Debug|Any CPU
60 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|x64.Build.0 = Debug|Any CPU
61 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|x64.Deploy.0 = Debug|Any CPU
62 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|x86.ActiveCfg = Debug|Any CPU
63 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|x86.Build.0 = Debug|Any CPU
64 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Debug|x86.Deploy.0 = Debug|Any CPU
65 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|Any CPU.ActiveCfg = Release|Any CPU
66 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|Any CPU.Build.0 = Release|Any CPU
67 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|Any CPU.Deploy.0 = Release|Any CPU
68 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|ARM.ActiveCfg = Release|Any CPU
69 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|ARM.Build.0 = Release|Any CPU
70 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|ARM.Deploy.0 = Release|Any CPU
71 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|ARM64.ActiveCfg = Release|Any CPU
72 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|ARM64.Build.0 = Release|Any CPU
73 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|ARM64.Deploy.0 = Release|Any CPU
74 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|x64.ActiveCfg = Release|Any CPU
75 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|x64.Build.0 = Release|Any CPU
76 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|x64.Deploy.0 = Release|Any CPU
77 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|x86.ActiveCfg = Release|Any CPU
78 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|x86.Build.0 = Release|Any CPU
79 | {1FFE1363-C8AC-4EE3-B83F-260ADB517F49}.Release|x86.Deploy.0 = Release|Any CPU
80 | EndGlobalSection
81 | GlobalSection(SolutionProperties) = preSolution
82 | HideSolutionNode = FALSE
83 | EndGlobalSection
84 | GlobalSection(ExtensibilityGlobals) = postSolution
85 | SolutionGuid = {25DAED5F-7622-4715-B52F-08735C64ED09}
86 | EndGlobalSection
87 | EndGlobal
88 |
--------------------------------------------------------------------------------
/QPlayer/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/QPlayer/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Configuration;
4 | using System.Data;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using System.Windows;
8 |
9 | namespace QPlayer
10 | {
11 | ///
12 | /// Interaction logic for App.xaml
13 | ///
14 | public partial class App : Application
15 | {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/QPlayer/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | [assembly: ThemeInfo(
4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
5 | //(used if a resource is not found in the page,
6 | // or application resource dictionaries)
7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
8 | //(used if a resource is not found in the page,
9 | // app, or any theme specific resource dictionaries)
10 | )]
11 |
--------------------------------------------------------------------------------
/QPlayer/Audio/EQSampleProvider.cs:
--------------------------------------------------------------------------------
1 | using NAudio.Wave;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace QPlayer.Audio;
9 |
10 | internal class EQSampleProvider : ISampleProvider
11 | {
12 | public WaveFormat WaveFormat => throw new NotImplementedException();
13 |
14 | public int Read(float[] buffer, int offset, int count)
15 | {
16 | throw new NotImplementedException();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/QPlayer/Audio/FadingSampleProvider.cs:
--------------------------------------------------------------------------------
1 | using JetBrains.Profiler.Api;
2 | using NAudio.Wave;
3 | using QPlayer.Audio;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | namespace QPlayer.Audio;
12 |
13 | internal class FadingSampleProvider : ISampleProvider
14 | {
15 | private readonly ISampleProvider source;
16 | private readonly object lockObj = new();
17 | private FadeState state;
18 | private long fadeTime;
19 | private long fadeDuration;
20 | private float startVolume = 1;
21 | private float endVolume = 1;
22 | private FadeType fadeType;
23 | private Action? onCompleteAction;
24 | private SynchronizationContext? synchronizationContext;
25 |
26 | public FadingSampleProvider(ISampleProvider source, bool startSilent = false)
27 | {
28 | this.source = source;
29 | state = FadeState.Ready;
30 | if (startSilent)
31 | startVolume = 0;
32 | else
33 | startVolume = 1;
34 | }
35 |
36 | public WaveFormat WaveFormat => source.WaveFormat;
37 |
38 | public float Volume
39 | {
40 | get => startVolume;
41 | set => startVolume = value;
42 | }
43 |
44 | public int Read(float[] buffer, int offset, int count)
45 | {
46 | int numSource = source.Read(buffer, offset, count);
47 | int num = numSource;
48 | lock (lockObj)
49 | {
50 | if (state == FadeState.Fading)
51 | {
52 | int numFaded = FadeSamples(buffer, offset, numSource);
53 | offset += numFaded;
54 | num -= numFaded;
55 | }
56 |
57 | if (startVolume == 0)
58 | {
59 | Array.Clear(buffer, offset, num);
60 | return numSource;
61 | }
62 | else if (startVolume == 1)
63 | {
64 | return numSource;
65 | }
66 |
67 | if (num > 0)
68 | VectorExtensions.Multiply(buffer.AsSpan(offset, num), startVolume);
69 | }
70 |
71 | return numSource;
72 | }
73 |
74 | ///
75 | /// Starts a new fade operation, cancelling any active fade operation.
76 | ///
77 | /// The volume to fade to
78 | /// The time to fade over in milliseconds
79 | /// The type of fade to use
80 | /// Optionally, an event to raise when the fade is completed. true is passed to the
81 | /// event handler if the fade completed normally, false if it was cancelled. The event is invoked on the
82 | /// thread that called this method.
83 | public void BeginFade(float volume, double durationMS, FadeType fadeType = FadeType.Linear, Action? onComplete = null)
84 | {
85 | lock (lockObj)
86 | {
87 | EndFade();
88 |
89 | fadeTime = 0;
90 | fadeDuration = (int)(durationMS * source.WaveFormat.SampleRate / 1000.0);
91 | endVolume = volume;
92 | this.fadeType = fadeType;
93 | onCompleteAction = onComplete;
94 | synchronizationContext = SynchronizationContext.Current;
95 | state = FadeState.Fading;
96 | }
97 | }
98 |
99 | ///
100 | /// Cancels the active fade operation.
101 | ///
102 | public void EndFade()
103 | {
104 | if (state != FadeState.Fading)
105 | return;
106 |
107 | lock (lockObj)
108 | {
109 | state = FadeState.Ready;
110 | float t = GetFadeFraction();
111 | startVolume = endVolume * t + startVolume * (1 - t);
112 | if (synchronizationContext != null)
113 | synchronizationContext.Post(x => onCompleteAction?.Invoke(false), null);
114 | else
115 | onCompleteAction?.Invoke(false);
116 | //onCompleteAction = null;
117 | //synchronizationContext = null;
118 | }
119 | }
120 |
121 | private int FadeSamples(float[] buffer, int offset, int count)
122 | {
123 | int i = 0;
124 | int channels = source.WaveFormat.Channels;
125 | if (fadeTime == 0)
126 | {
127 | //var dbg_t = DateTime.Now;
128 | //MainViewModel.Log($"[Playback Debugging] Audio fade start! {dbg_t:HH:mm:ss.ffff} dt={(dbg_t - MainViewModel.dbg_cueStartTime)}");
129 | MeasureProfiler.SaveData();
130 | }
131 | while (i < count)
132 | {
133 | if (fadeTime >= fadeDuration)
134 | {
135 | startVolume = endVolume;
136 | state = FadeState.Ready;
137 | if (synchronizationContext != null)
138 | synchronizationContext.Post(x => onCompleteAction?.Invoke(true), null);
139 | else
140 | onCompleteAction?.Invoke(true);
141 | //onCompleteAction = null;
142 | //synchronizationContext = null;
143 |
144 | break;
145 | }
146 |
147 | float t = GetFadeFraction();
148 | float currVol = endVolume * t + startVolume * (1 - t);
149 | for (int c = 0; c < channels; c++)
150 | buffer[offset + i + c] *= currVol;
151 |
152 | i += channels;
153 | fadeTime++;
154 | }
155 |
156 | return i;
157 | }
158 |
159 | private float GetFadeFraction()
160 | {
161 | float t = fadeTime / (float)fadeDuration;
162 | switch (fadeType)
163 | {
164 | case FadeType.SCurve:
165 | // Cubic hermite spline
166 | float t2 = t * t;
167 | float t3 = t2 * t;
168 | t = -2 * t3 + 3 * t2;
169 | break;
170 | case FadeType.Square:
171 | t *= t;
172 | break;
173 | case FadeType.InverseSquare:
174 | t = MathF.Sqrt(t);
175 | break;
176 | case FadeType.Linear:
177 | default:
178 | break;
179 | }
180 | return t;
181 | }
182 | }
183 |
184 | internal enum FadeState
185 | {
186 | Ready,
187 | Fading
188 | }
189 |
190 | public enum FadeType
191 | {
192 | Linear,
193 | SCurve,
194 | Square,
195 | InverseSquare,
196 | }
197 |
--------------------------------------------------------------------------------
/QPlayer/Audio/LoopingSampleProvider.cs:
--------------------------------------------------------------------------------
1 | using NAudio.Wave;
2 | using System;
3 |
4 | namespace QPlayer.Audio;
5 |
6 | public class LoopingSampleProvider : WaveStream, ISampleProvider where T : WaveStream, ISampleProvider
7 | {
8 | private readonly T input;
9 | private readonly bool infinite;
10 | private readonly int loops;
11 | private long playedLoops = 0;
12 |
13 | public LoopingSampleProvider(T input, bool infinite = true, int loops = 1)
14 | {
15 | this.input = input;
16 | this.infinite = infinite;
17 | this.loops = loops;
18 | }
19 |
20 | public override WaveFormat WaveFormat => input.WaveFormat;
21 |
22 | public override long Length => infinite ? long.MaxValue / 32 : input.Length * loops;
23 |
24 | public override long Position
25 | {
26 | get => input.Position;
27 | set
28 | {
29 | input.Position = value % input.Length;
30 | playedLoops = value / input.Length;
31 | }
32 | }
33 |
34 | public int Read(float[] buffer, int offset, int count)
35 | {
36 | int samplesRead = 0;
37 | int readOffset = offset;
38 | while (samplesRead < count)
39 | {
40 | int read = input.Read(buffer, readOffset, count - samplesRead);
41 | readOffset += read;
42 | if (read == 0)
43 | {
44 | input.Position = 0;
45 | readOffset = 0;
46 | playedLoops++;
47 |
48 | if (!infinite && playedLoops == loops)
49 | break;
50 | }
51 | }
52 | return samplesRead;
53 | }
54 |
55 | public override int Read(byte[] buffer, int offset, int count)
56 | {
57 | int samplesRead = 0;
58 | int readOffset = offset;
59 | while (samplesRead < count)
60 | {
61 | int read = input.Read(buffer, readOffset, count - samplesRead);
62 | readOffset += read;
63 | if (read == 0)
64 | {
65 | input.Position = 0;
66 | readOffset = 0;
67 | playedLoops++;
68 |
69 | if (!infinite && playedLoops == loops)
70 | break;
71 | }
72 | }
73 | return samplesRead;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/QPlayer/Audio/MediaCacheManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace QPlayer.Audio;
10 |
11 | internal class MediaCacheManager
12 | {
13 | private ConcurrentDictionary cacheDict;
14 |
15 | public MediaCacheManager()
16 | {
17 | cacheDict = new();
18 | }
19 | }
20 |
21 | internal class CachedMediaStream : MemoryStream, IDisposable
22 | {
23 | protected int references = 0;
24 |
25 | protected override void Dispose(bool disposing)
26 | {
27 | base.Dispose(disposing);
28 | }
29 |
30 | void IDisposable.Dispose()
31 | {
32 |
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/QPlayer/Audio/VectorExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Numerics;
7 | using System.Runtime.CompilerServices;
8 | using System.Runtime.InteropServices;
9 |
10 | namespace QPlayer.Audio;
11 |
12 | internal static class VectorExtensions
13 | {
14 | ///
15 | /// Multiplies the contents of span a by the scalar b, mutating a with the result.
16 | ///
17 | ///
18 | ///
19 | ///
20 | public static void Multiply(Span a, T b)
21 | where T : INumber
22 | {
23 | int len = a.Length;
24 | int i = 0;
25 | if (len < Vector.Count || !Vector.IsHardwareAccelerated)
26 | goto CopyRemaining;
27 |
28 | int remaining = len % Vector.Count;
29 |
30 | var vecB = new Vector(b);
31 | ref T dst = ref MemoryMarshal.GetReference(a);
32 | /*
33 | Inner loop compiles to (x86_64 with AVX2, .NET 8, PGO Tier 1):
34 | vmulps ymm2, ymm0, ymmword ptr [r8]
35 | vmovups ymmword ptr [r8], ymm2
36 | add r8, 32
37 | add r9d, 8
38 | cmp r11d, r9d
39 | */
40 | for (; i < len - remaining; i += Vector.Count)
41 | {
42 | //var vecA = new Vector(ref dst);
43 | var vecA = Vector.LoadUnsafe(ref dst);
44 | var res = Vector.Multiply(vecA, vecB);
45 | ref var dstByte = ref Unsafe.As(ref dst);
46 | Unsafe.WriteUnaligned(ref dstByte, res);
47 | dst = ref Unsafe.Add(ref dst, Vector.Count);
48 | }
49 |
50 | CopyRemaining:
51 | for (; i < len; i++)
52 | a[i] *= b;
53 | }
54 |
55 | ///
56 | /// Multiplies the pairs of values in a by the values in b, mutating a with the result.
57 | ///
58 | ///
59 | ///
60 | ///
61 | public static void Multiply(Span a, Span b)
62 | where T : INumber
63 | {
64 | int len = a.Length;
65 | ArgumentOutOfRangeException.ThrowIfNotEqual(len, b.Length);
66 | int i = 0;
67 | if (len < Vector.Count || !Vector.IsHardwareAccelerated)
68 | goto CopyRemaining;
69 |
70 | int remaining = len % Vector.Count;
71 |
72 | ref T dst = ref MemoryMarshal.GetReference(a);
73 | ref T bRef = ref MemoryMarshal.GetReference(b);
74 | for (; i < len - remaining; i += Vector.Count)
75 | {
76 | var vecA = Vector.LoadUnsafe(ref dst);
77 | var vecB = Vector.LoadUnsafe(ref bRef);
78 |
79 | var res = Vector.Multiply(vecA, vecB);
80 | ref var dstByte = ref Unsafe.As(ref dst);
81 | Unsafe.WriteUnaligned(ref dstByte, res);
82 |
83 | dst = ref Unsafe.Add(ref dst, Vector.Count);
84 | bRef = ref Unsafe.Add(ref bRef, Vector.Count);
85 | }
86 |
87 | CopyRemaining:
88 | for (; i < len; i++)
89 | a[i] *= b[i];
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/QPlayer/FodyWeavers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/QPlayer/FodyWeavers.xsd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Used to control if the On_PropertyName_Changed feature is enabled.
13 |
14 |
15 |
16 |
17 | Used to control if the Dependent properties feature is enabled.
18 |
19 |
20 |
21 |
22 | Used to control if the IsChanged property feature is enabled.
23 |
24 |
25 |
26 |
27 | Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.
28 |
29 |
30 |
31 |
32 | Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.
33 |
34 |
35 |
36 |
37 | Used to control if equality checks should use the Equals method resolved from the base class.
38 |
39 |
40 |
41 |
42 | Used to control if equality checks should use the static Equals method resolved from the base class.
43 |
44 |
45 |
46 |
47 | Used to turn off build warnings from this weaver.
48 |
49 |
50 |
51 |
52 | Used to turn off build warnings about mismatched On_PropertyName_Changed methods.
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
61 |
62 |
63 |
64 |
65 | A comma-separated list of error codes that can be safely ignored in assembly verification.
66 |
67 |
68 |
69 |
70 | 'false' to turn off automatic generation of the XML Schema file.
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/QPlayer/Models/OSCDriver.cs:
--------------------------------------------------------------------------------
1 | using Rug.Osc;
2 | using System;
3 | using System.Collections.Concurrent;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Net;
7 | using System.Net.Sockets;
8 | using System.Text;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using static QPlayer.ViewModels.MainViewModel;
12 |
13 | namespace QPlayer.Models;
14 |
15 | internal class OSCDriver : IDisposable
16 | {
17 | //public ConcurrentQueue RXMessages { get; private set; } = [];
18 | public event Action? OnRXMessage;
19 | public event Action? OnTXMessage;
20 |
21 | private OscReceiver? oscReceiver;
22 | private OscSender? oscSender;
23 |
24 | private IPEndPoint? rxIP;
25 | private IPEndPoint? txIP;
26 |
27 | private readonly OSCAddressRouter router = new();
28 |
29 | public bool OSCConnect(IPAddress nicAddress, int rxPort, int txPort)
30 | {
31 | //RXMessages.Clear();
32 |
33 | rxIP = new IPEndPoint(nicAddress ?? IPAddress.Any, rxPort);
34 | txIP = new IPEndPoint(nicAddress ?? IPAddress.Broadcast, txPort);
35 |
36 | Log($"Connecting to OSC... rx={rxIP} tx={txIP}");
37 |
38 | try
39 | {
40 | Dispose();
41 |
42 | oscReceiver = new(rxIP.Address, rxIP.Port);
43 | oscReceiver.Connect();
44 |
45 | oscSender = new(txIP.Address, 0, txIP.Port);
46 | oscSender.Connect();
47 |
48 | Task.Run(OSCRXThread);
49 | }
50 | catch (Exception e)
51 | {
52 | Log($"Failed to connect to OSC port: {e}", LogLevel.Error);
53 | return false;
54 | }
55 |
56 | Log("Connected to OSC network!");
57 |
58 | return true;
59 | }
60 |
61 | ///
62 | /// Asynchronously sends an OSC packet.
63 | ///
64 | ///
65 | public void SendMessage(OscPacket packet)
66 | {
67 | OnTXMessage?.Invoke(packet);
68 | oscSender?.Send(packet);
69 | }
70 |
71 | ///
72 | /// Subscribes an event handler to OSC messages following a specified pattern.
73 | ///
74 | ///
75 | /// Address patterns are of the form: "/foo/?/bar"
76 | ///
77 | /// Where '?' indicates a wildcard which matches any single address part.
78 | ///
79 | /// The adddress pattern to match.
80 | /// The event handler to fire when a matching message is received.
81 | public void Subscribe(string pattern, Action handler, SynchronizationContext? syncContext = null)
82 | {
83 | OSCAddressRouter.Subscribe(router, pattern, handler, syncContext);
84 | }
85 |
86 | private void OSCRXThread()
87 | {
88 | while (oscReceiver?.State == OscSocketState.Connected)
89 | {
90 | try
91 | {
92 | var pkt = oscReceiver.Receive();
93 | //RXMessages.Enqueue(pkt);
94 | OnRXMessage?.Invoke(pkt);
95 | if (pkt is OscMessage msg && msg.Address.Length > 0)
96 | router.ReceiveMessage(msg, msg.Address.AsSpan(1));
97 |
98 | //Log($"Recv osc msg: {pkt}", LogLevel.Debug);
99 | }
100 | catch (Exception e)
101 | {
102 | Log($"OSC Network connection lost: {e}", LogLevel.Warning);
103 | }
104 | }
105 | }
106 |
107 | public void Dispose()
108 | {
109 | /*if (OnRXMessage != null)
110 | foreach (var d in OnRXMessage.GetInvocationList())
111 | OnRXMessage -= d as Action;
112 | if (OnTXMessage != null)
113 | foreach (var d in OnTXMessage.GetInvocationList())
114 | OnTXMessage -= d as Action;*/
115 |
116 | oscReceiver?.Dispose();
117 | oscSender?.Dispose();
118 | oscReceiver = null;
119 | oscSender = null;
120 | }
121 | }
122 |
123 | public class OSCAddressRouter
124 | {
125 | public Dictionary? children;
126 | public event Action? OnRXMessage;
127 |
128 | public static void Subscribe(OSCAddressRouter root, string pattern, Action handler, SynchronizationContext? syncContext = null)
129 | {
130 | var parts = pattern.Split('/');
131 | OSCAddressRouter router = root;
132 | for (int i = 1; i < parts.Length; i++)
133 | {
134 | string part = parts[i];
135 | if (router.children?.TryGetValue(part, out var child) ?? false)
136 | {
137 | router = child;
138 | }
139 | else
140 | {
141 | router.children ??= [];
142 | var nRouter = new OSCAddressRouter();
143 | router.children.Add(part, nRouter);
144 | router = nRouter;
145 | }
146 | }
147 | if (syncContext != null)
148 | router.OnRXMessage += msg => syncContext.Post(_=>handler(msg), null);
149 | else
150 | router.OnRXMessage += handler;
151 | }
152 |
153 | public void ReceiveMessage(OscMessage message, ReadOnlySpan addressSpan)
154 | {
155 | OnRXMessage?.Invoke(message);
156 |
157 | if (children != null)
158 | {
159 | int slashPos = addressSpan.IndexOf('/');
160 | string key;
161 | if (slashPos != -1)
162 | key = new(addressSpan[..slashPos]);
163 | else
164 | key = new(addressSpan);
165 |
166 | if (children.TryGetValue(key, out var child))
167 | child.ReceiveMessage(message, addressSpan[(slashPos + 1)..]);
168 | if (children.TryGetValue("?", out var child1))
169 | child1.ReceiveMessage(message, addressSpan[(slashPos + 1)..]);
170 | }
171 | }
172 | }
173 |
174 | public static class OSCMessageParser
175 | {
176 | ///
177 | /// Parses a string representing an OSC message into an address and a list of arguments.
178 | /// Arguments must be separated by spaces and are parsed automatically
179 | /// Supports:
180 | /// - strings -> Surrounded by double quotes
181 | /// - ints
182 | /// - floats
183 | /// - bools
184 | /// - blobs -> Surrounded by backticks
185 | ///
186 | ///
187 | ///
188 | public static (string address, object[] args) ParseOSCMessage(string message)
189 | {
190 | ReadOnlyMemory msg = message.AsMemory();
191 | int argsStart = message.IndexOf(' ');
192 | var args = new List