├── .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(); 193 | string address = message; 194 | if (argsStart != -1) 195 | { 196 | address = message[..argsStart]; 197 | var strArgs = message[(argsStart + 1)..].Split(' '); 198 | for (int i = 0; i < strArgs.Length; i++) 199 | { 200 | if (bool.TryParse(strArgs[i], out var bVal)) 201 | args.Add(bVal); 202 | else if (int.TryParse(strArgs[i], out var iVal)) 203 | args.Add(iVal); 204 | else if (float.TryParse(strArgs[i], out var fVal)) 205 | args.Add(fVal); 206 | else if (strArgs[i].Length > 0 && strArgs[i][0] == '\"') 207 | { 208 | if (strArgs[i].Length > 1 && strArgs[i][^1] == '\"') 209 | { 210 | args.Add(strArgs[i][1..^1]); 211 | } 212 | else 213 | { 214 | // String must have spaces in it, search for the next arg that ends in a double quote 215 | StringBuilder sb = new(strArgs[i][1..]); 216 | do 217 | { 218 | i++; 219 | sb.Append(' '); 220 | sb.Append(strArgs[i]); 221 | } while (i < strArgs.Length && strArgs[i][^1] != '\"'); 222 | 223 | if (strArgs[i][^1] != '\"') 224 | throw new ArgumentException($"Unparsable OSC argument, string is not closed: {sb.ToString()}"); 225 | 226 | args.Add(sb.ToString()); 227 | } 228 | } 229 | else if (strArgs[i].Length > 3 && strArgs[i][0] == '`' && strArgs[i][^1] == '`') 230 | { 231 | args.Add(StringToByteArrayFastest(strArgs[i][1..^1])); 232 | } 233 | else 234 | { 235 | throw new ArgumentException($"Unparsable OSC argument encountered: {strArgs[i]}"); 236 | } 237 | } 238 | } 239 | 240 | return (address, args.ToArray()); 241 | } 242 | 243 | // https://stackoverflow.com/a/9995303 244 | private static byte[] StringToByteArrayFastest(string hex) 245 | { 246 | if (hex.Length % 2 == 1) 247 | throw new Exception("The binary key cannot have an odd number of digits"); 248 | 249 | byte[] arr = new byte[hex.Length >> 1]; 250 | 251 | for (int i = 0; i < hex.Length >> 1; ++i) 252 | { 253 | arr[i] = (byte)((GetHexVal(hex[i << 1]) << 4) + (GetHexVal(hex[(i << 1) + 1]))); 254 | } 255 | 256 | return arr; 257 | } 258 | 259 | private static int GetHexVal(char hex) 260 | { 261 | int val = (int)hex; 262 | //For uppercase A-F letters: 263 | //return val - (val < 58 ? 48 : 55); 264 | //For lowercase a-f letters: 265 | //return val - (val < 58 ? 48 : 87); 266 | //Or the two combined, but a bit slower: 267 | return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /QPlayer/Models/ShowFile.cs: -------------------------------------------------------------------------------- 1 | using QPlayer.Audio; 2 | using QPlayer.ViewModels; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Drawing; 6 | using System.Text.Json.Serialization; 7 | 8 | namespace QPlayer.Models; 9 | 10 | [Serializable] 11 | public record ShowFile 12 | { 13 | public const int FILE_FORMAT_VERSION = 2; 14 | 15 | public int fileFormatVersion = FILE_FORMAT_VERSION; 16 | public ShowMetadata showMetadata = new(); 17 | public List columnWidths = []; 18 | public List cues = [new SoundCue()]; 19 | } 20 | 21 | [Serializable] 22 | public record ShowMetadata 23 | { 24 | public string title = "Untitled"; 25 | public string description = ""; 26 | public string author = ""; 27 | public DateTime date = DateTime.Today; 28 | 29 | public int audioLatency = 100; 30 | public AudioOutputDriver audioOutputDriver; 31 | public string audioOutputDevice = ""; 32 | 33 | public string oscNIC = ""; 34 | public int oscRXPort = 9000; 35 | public int oscTXPort = 8000; 36 | } 37 | 38 | public enum CueType 39 | { 40 | None, 41 | GroupCue, 42 | DummyCue, 43 | SoundCue, 44 | TimeCodeCue, 45 | StopCue, 46 | VolumeCue 47 | } 48 | 49 | public enum LoopMode 50 | { 51 | OneShot, 52 | Looped, 53 | LoopedInfinite, 54 | } 55 | 56 | public enum StopMode 57 | { 58 | Immediate, 59 | LoopEnd 60 | } 61 | 62 | [Serializable] 63 | [JsonPolymorphic(IgnoreUnrecognizedTypeDiscriminators = true, UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)] 64 | [JsonDerivedType(typeof(Cue), typeDiscriminator: nameof(Cue))] 65 | [JsonDerivedType(typeof(GroupCue), typeDiscriminator: nameof(GroupCue))] 66 | [JsonDerivedType(typeof(DummyCue), typeDiscriminator: nameof(DummyCue))] 67 | [JsonDerivedType(typeof(SoundCue), typeDiscriminator: nameof(SoundCue))] 68 | [JsonDerivedType(typeof(TimeCodeCue), typeDiscriminator: nameof(TimeCodeCue))] 69 | [JsonDerivedType(typeof(StopCue), typeDiscriminator: nameof(StopCue))] 70 | [JsonDerivedType(typeof(VolumeCue), typeDiscriminator: nameof(VolumeCue))] 71 | public record Cue 72 | { 73 | public CueType type; 74 | public decimal qid; 75 | public decimal? parent; 76 | public Color colour = Color.Black; 77 | public string name = string.Empty; 78 | public string description = string.Empty; 79 | public bool halt = true; 80 | public bool enabled = true; 81 | public TimeSpan delay; 82 | public LoopMode loopMode; 83 | public int loopCount; 84 | } 85 | 86 | [Serializable] 87 | [JsonDerivedType(typeof(GroupCue), typeDiscriminator: nameof(GroupCue))] 88 | public record GroupCue : Cue 89 | { 90 | public GroupCue() : base() 91 | { 92 | type = CueType.GroupCue; 93 | } 94 | } 95 | 96 | [Serializable] 97 | [JsonDerivedType(typeof(DummyCue), typeDiscriminator: nameof(DummyCue))] 98 | public record DummyCue : Cue 99 | { 100 | public DummyCue() : base() 101 | { 102 | type = CueType.DummyCue; 103 | } 104 | } 105 | 106 | [Serializable] 107 | [JsonDerivedType(typeof(SoundCue), typeDiscriminator: nameof(SoundCue))] 108 | public record SoundCue : Cue 109 | { 110 | public string path = string.Empty; 111 | public TimeSpan startTime; 112 | public TimeSpan duration; 113 | public float volume = 1; 114 | public float fadeIn; 115 | public float fadeOut; 116 | public FadeType fadeType; 117 | 118 | public SoundCue() : base() 119 | { 120 | type = CueType.SoundCue; 121 | } 122 | } 123 | 124 | [Serializable] 125 | [JsonDerivedType(typeof(TimeCodeCue), typeDiscriminator: nameof(TimeCodeCue))] 126 | public record TimeCodeCue : Cue 127 | { 128 | public TimeSpan startTime; 129 | public TimeSpan duration; 130 | 131 | public TimeCodeCue() : base() 132 | { 133 | type = CueType.TimeCodeCue; 134 | } 135 | } 136 | 137 | [Serializable] 138 | [JsonDerivedType(typeof(StopCue), typeDiscriminator: nameof(StopCue))] 139 | public record StopCue : Cue 140 | { 141 | public decimal stopQid; 142 | public StopMode stopMode; 143 | public float fadeOutTime; 144 | public FadeType fadeType; 145 | 146 | public StopCue() : base() 147 | { 148 | type = CueType.StopCue; 149 | } 150 | } 151 | 152 | [Serializable] 153 | [JsonDerivedType(typeof(VolumeCue), typeDiscriminator: nameof(VolumeCue))] 154 | public record VolumeCue : Cue 155 | { 156 | public decimal soundQid; 157 | public float fadeTime; 158 | public float volume; 159 | public FadeType fadeType; 160 | 161 | public VolumeCue() : base() 162 | { 163 | type = CueType.VolumeCue; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /QPlayer/Properties/PublishProfiles/ClickOnceProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | 2 8 | 1.6.2.* 9 | True 10 | Release 11 | True 12 | False 13 | true 14 | True 15 | Disk 16 | True 17 | False 18 | 256BA6FA05B0E90D8F154AE963662DA87EAC102C 19 | True 20 | False 21 | Any CPU 22 | QPlayer 23 | bin\Release\net8.0-windows\app.publish\ 24 | ..\publish\ 25 | Thomas Mathieson 26 | ClickOnce 27 | False 28 | False 29 | False 30 | sha256RSA 31 | True 32 | false 33 | https://github.com/space928/QPlayer/issues 34 | net8.0-windows 35 | False 36 | Foreground 37 | False 38 | Publish.html 39 | cert.pfx 40 | 41 | 42 | 43 | true 44 | .NET Desktop Runtime 8.0.5 (x64) 45 | 46 | 47 | 48 | 49 | False 50 | QPlayer Project 51 | 1 52 | Icon.ico 53 | 54 | 55 | -------------------------------------------------------------------------------- /QPlayer/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | ..\publish\ 10 | FileSystem 11 | <_TargetId>Folder 12 | net8.0-windows 13 | false 14 | 15 | -------------------------------------------------------------------------------- /QPlayer/QPlayer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net8.0-windows 6 | enable 7 | true 8 | QPlayer 9 | 1.7.3-beta 10 | Thomas Mathieson 11 | Thomas Mathieson 12 | ©️ Thomas Mathieson 2024 13 | https://github.com/space928/QPlayer 14 | https://github.com/space928/QPlayer 15 | GPL-3.0-or-later 16 | Resources\Icon.ico 17 | AnyCPU;x86;x64 18 | true 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/Icon.ico -------------------------------------------------------------------------------- /QPlayer/Resources/Icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/Icon.psd -------------------------------------------------------------------------------- /QPlayer/Resources/IconL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/IconL.png -------------------------------------------------------------------------------- /QPlayer/Resources/IconM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/IconM.png -------------------------------------------------------------------------------- /QPlayer/Resources/IconS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/IconS.png -------------------------------------------------------------------------------- /QPlayer/Resources/IconXL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/IconXL.png -------------------------------------------------------------------------------- /QPlayer/Resources/IconXS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/IconXS.png -------------------------------------------------------------------------------- /QPlayer/Resources/IconXXS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/IconXXS.png -------------------------------------------------------------------------------- /QPlayer/Resources/IconXXXS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/IconXXXS.png -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/.gitignore: -------------------------------------------------------------------------------- 1 | /SvgToXaml/ 2 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/ConvertedIcons.xaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/Icons/ConvertedIcons.xaml -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/Update.ps1: -------------------------------------------------------------------------------- 1 | # SvgToXaml somehow, for some reason writes to standard *input* when it exits. So we have to do this junk so that it doesn't break the rest of the script... 2 | Start-Process "SvgToXaml\SvgToXaml.exe" -ArgumentList 'BuildDict /inputdir "." /outputdir "." /outputname ConvertedIcons /nameprefix "Icon" /buildhtmlfile=false' -Wait -UseNewEnvironment 3 | 4 | (Get-Content .\ConvertedIcons.xaml) -replace 'Brush=\"#.*?\"','Brush="{StaticResource IconColor}"' | Out-File .\ConvertedIcons.xaml 5 | Write-Output "Updated icons!" 6 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/arrow-right-long-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/circle-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/clock-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/folder-open-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/folder-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/gear-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/minus-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/music-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/pause-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/play-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/plus-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/rotate-right-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/rotate-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/sort-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/stop-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Icons/volume-low-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/Logo.ai -------------------------------------------------------------------------------- /QPlayer/Resources/Logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/LogoSmall.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /QPlayer/Resources/Splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/Splash.png -------------------------------------------------------------------------------- /QPlayer/Resources/Splash.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/QPlayer/Resources/Splash.psd -------------------------------------------------------------------------------- /QPlayer/ThemesV2/Controls.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace REghZy.Themes { 4 | public partial class Controls { 5 | private void CloseWindow_Event(object sender, RoutedEventArgs e) { 6 | if (e.Source != null) 7 | try { CloseWind(Window.GetWindow((FrameworkElement)e.Source)); } 8 | catch { } 9 | } 10 | private void AutoMinimize_Event(object sender, RoutedEventArgs e) { 11 | if (e.Source != null) 12 | try { MaximizeRestore(Window.GetWindow((FrameworkElement)e.Source)); } 13 | catch { } 14 | } 15 | private void Minimize_Event(object sender, RoutedEventArgs e) { 16 | if (e.Source != null) 17 | try { MinimizeWind(Window.GetWindow((FrameworkElement)e.Source)); } 18 | catch { } 19 | } 20 | 21 | public void CloseWind(Window window) => window.Close(); 22 | public void MaximizeRestore(Window window) { 23 | if (window.WindowState == WindowState.Maximized) 24 | window.WindowState = WindowState.Normal; 25 | else if (window.WindowState == WindowState.Normal) 26 | window.WindowState = WindowState.Maximized; 27 | } 28 | public void MinimizeWind(Window window) => window.WindowState = WindowState.Minimized; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /QPlayer/ThemesV2/CustomControls.xaml: -------------------------------------------------------------------------------- 1 |  3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | 43 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | 58 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 85 | -------------------------------------------------------------------------------- /QPlayer/ThemesV2/Icons.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 7 | ../Resources/Icon.ico 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /QPlayer/ThemesV2/ThemeTypes.cs: -------------------------------------------------------------------------------- 1 | namespace REghZy.Themes; 2 | 3 | public enum ThemeType 4 | { 5 | Dark, 6 | Red, 7 | Light, 8 | } 9 | 10 | public static class ThemeTypeExtension 11 | { 12 | public static string? GetName(this ThemeType type) 13 | { 14 | return type switch 15 | { 16 | ThemeType.Light => "Dark_DarkBackLightBorder", 17 | ThemeType.Dark => "Dark_DarkBackDarkBorder", 18 | ThemeType.Red => "RedBlackTheme", 19 | _ => null, 20 | }; 21 | } 22 | } -------------------------------------------------------------------------------- /QPlayer/ThemesV2/ThemesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Media; 4 | 5 | namespace REghZy.Themes; 6 | 7 | public static class ThemesController 8 | { 9 | public static ThemeType CurrentTheme { get; set; } 10 | 11 | private static ResourceDictionary ThemeDictionary 12 | { 13 | get => Application.Current.Resources.MergedDictionaries[0]; 14 | set => Application.Current.Resources.MergedDictionaries[0] = value; 15 | } 16 | 17 | private static ResourceDictionary ControlColours 18 | { 19 | get => Application.Current.Resources.MergedDictionaries[1]; 20 | set => Application.Current.Resources.MergedDictionaries[1] = value; 21 | } 22 | 23 | private static ResourceDictionary Controls 24 | { 25 | get => Application.Current.Resources.MergedDictionaries[2]; 26 | set => Application.Current.Resources.MergedDictionaries[2] = value; 27 | } 28 | 29 | public static void SetTheme(ThemeType theme) 30 | { 31 | string? themeName = theme.GetName(); 32 | CurrentTheme = theme; 33 | if (string.IsNullOrEmpty(themeName)) 34 | return; 35 | 36 | ThemeDictionary = new ResourceDictionary() { Source = new Uri($"Themes/{themeName}.xaml", UriKind.Relative) }; 37 | ControlColours = new ResourceDictionary() { Source = new Uri("Themes/ControlColours.xaml", UriKind.Relative) }; 38 | Controls = new ResourceDictionary() { Source = new Uri("Themes/Controls.xaml", UriKind.Relative) }; 39 | } 40 | 41 | public static object GetResource(object key) 42 | { 43 | return ThemeDictionary[key]; 44 | } 45 | 46 | public static SolidColorBrush GetBrush(string name) 47 | { 48 | return GetResource(name) is SolidColorBrush brush ? brush : new SolidColorBrush(Colors.White); 49 | } 50 | } -------------------------------------------------------------------------------- /QPlayer/ViewModels/DummyCueViewModel.cs: -------------------------------------------------------------------------------- 1 | using QPlayer.Models; 2 | using Cue = QPlayer.Models.Cue; 3 | 4 | namespace QPlayer.ViewModels 5 | { 6 | public class DummyCueViewModel : CueViewModel, IConvertibleModel 7 | { 8 | public DummyCueViewModel(MainViewModel mainViewModel) : base(mainViewModel) 9 | { 10 | } 11 | 12 | public override void ToModel(Cue cue) 13 | { 14 | base.ToModel(cue); 15 | } 16 | 17 | public static new CueViewModel FromModel(Cue cue, MainViewModel mainViewModel) 18 | { 19 | DummyCueViewModel vm = new(mainViewModel); 20 | if (cue is DummyCue dcue) 21 | { 22 | // 23 | } 24 | return vm; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /QPlayer/ViewModels/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using ColorPicker.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Drawing; 5 | using System.Linq; 6 | using System.Numerics; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace QPlayer.ViewModels; 11 | 12 | public static partial class ExtensionMethods 13 | { 14 | public static Color ToColor(this ColorState x) 15 | { 16 | return Color.FromArgb(255, (byte)(x.RGB_R * 255), (byte)(x.RGB_G * 255), (byte)(x.RGB_B * 255)); 17 | } 18 | 19 | public static ColorState ToColorState(this Color x) 20 | { 21 | ColorState c = new();/*new() 22 | { 23 | A=1, 24 | RGB_R=x.r/255d, 25 | RGB_G=x.g/255d, 26 | RGB_B=x.b/255d 27 | };*/ 28 | c.SetARGB(1, x.R / 255d, x.G / 255d, x.B / 255d); 29 | 30 | return c; 31 | } 32 | 33 | /// 34 | /// Removes trailing zeros from a decimal. 35 | /// 36 | /// 37 | /// https://stackoverflow.com/a/7983330/10874820 38 | /// 39 | public static decimal Normalize(this decimal value) 40 | { 41 | return value / 1.000000000000000000000000000000000m; 42 | } 43 | 44 | /// 45 | /// Updates an item at the given index in the list, expanding the list with 46 | /// default items if index larger than the size of the list. 47 | /// 48 | /// 49 | /// 50 | /// 51 | /// 52 | public static void AddOrUpdate(this List list, int index, T value) 53 | { 54 | while (index >= list.Count) 55 | list.Add(default!); 56 | 57 | list[index] = value; 58 | } 59 | 60 | /// 61 | /// Sets the translation component of this matrix. 62 | /// 63 | /// 64 | /// 65 | /// 66 | public static Matrix4x4 SetTranslation(this Matrix4x4 mat, Vector3 trans) 67 | { 68 | mat.M41 = trans.X; 69 | mat.M42 = trans.Y; 70 | mat.M43 = trans.Z; 71 | return mat; 72 | } 73 | 74 | public static Vector3 XZY(this Vector3 value) => new(value.X, value.Z, value.Y); 75 | public static Vector3 YXZ(this Vector3 value) => new(value.Y, value.X, value.Z); 76 | public static Vector3 ZYX(this Vector3 value) => new(value.Z, value.Y, value.X); 77 | 78 | public static Vector4 XZYW(this Vector4 value) => new(value.X, value.Z, value.Y, value.W); 79 | public static Vector4 YXZW(this Vector4 value) => new(value.Y, value.X, value.Z, value.W); 80 | public static Vector4 ZYXW(this Vector4 value) => new(value.Z, value.Y, value.X, value.W); 81 | public static Vector4 WZYX(this Vector4 value) => new(value.W, value.Z, value.Y, value.X); 82 | } 83 | -------------------------------------------------------------------------------- /QPlayer/ViewModels/GroupCueViewModel.cs: -------------------------------------------------------------------------------- 1 | using QPlayer.Models; 2 | using Cue = QPlayer.Models.Cue; 3 | 4 | namespace QPlayer.ViewModels 5 | { 6 | public class GroupCueViewModel : CueViewModel, IConvertibleModel 7 | { 8 | public GroupCueViewModel(MainViewModel mainViewModel) : base(mainViewModel) 9 | { 10 | } 11 | 12 | public override void ToModel(Cue cue) 13 | { 14 | base.ToModel(cue); 15 | } 16 | 17 | public static new CueViewModel FromModel(Cue cue, MainViewModel mainViewModel) 18 | { 19 | GroupCueViewModel vm = new(mainViewModel); 20 | if (cue is GroupCue gcue) 21 | { 22 | // 23 | } 24 | return vm; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /QPlayer/ViewModels/ProjectSettingsViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using QPlayer.Models; 3 | using ReactiveUI.Fody.Helpers; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.ObjectModel; 7 | using System.Linq; 8 | using System.Net.NetworkInformation; 9 | using System.Net.Sockets; 10 | using System.Net; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | using QPlayer.Audio; 14 | 15 | namespace QPlayer.ViewModels; 16 | 17 | public class ProjectSettingsViewModel : ObservableObject, IConvertibleModel 18 | { 19 | #region Bindable Properties 20 | [Reactive] public string Title { get; set; } = "Untitled"; 21 | [Reactive] public string Description { get; set; } = string.Empty; 22 | [Reactive] public string Author { get; set; } = string.Empty; 23 | [Reactive] public DateTime Date { get; set; } 24 | 25 | [Reactive] public int AudioLatency { get; set; } 26 | [Reactive] public AudioOutputDriver AudioOutputDriver { get; set; } 27 | [Reactive] public int SelectedAudioOutputDeviceIndex { get; set; } 28 | 29 | [Reactive] public static ObservableCollection? AudioOutputDriverValues { get; private set; } 30 | [Reactive, ReactiveDependency(nameof(AudioOutputDriver))] 31 | public ObservableCollection AudioOutputDevices 32 | { 33 | get 34 | { 35 | audioOutputDevices = mainViewModel.AudioPlaybackManager.GetOutputDevices(AudioOutputDriver); 36 | return new(audioOutputDevices.Select(x => x.identifier)); 37 | } 38 | } 39 | 40 | [Reactive] public ObservableCollection NICs => nics; 41 | [Reactive] public int SelectedNIC { get; set; } 42 | [Reactive] public int OSCRXPort { get; set; } = 9000; 43 | [Reactive] public int OSCTXPort { get; set; } = 8000; 44 | [Reactive] public bool MonitorOSCMessages { get; set; } = false; 45 | #endregion 46 | 47 | public IPAddress OSCNic => (SelectedNIC >= 0 && SelectedNIC < nicAddresses.Count) ? nicAddresses[SelectedNIC] : (nicAddresses.FirstOrDefault() ?? IPAddress.Any); 48 | internal object? SelectedAudioOutputDeviceKey 49 | { 50 | get 51 | { 52 | var i = Math.Max(0, SelectedAudioOutputDeviceIndex); 53 | if (i >= audioOutputDevices.Length) 54 | return null; 55 | return audioOutputDevices[i].key; 56 | } 57 | } 58 | 59 | private (object key, string identifier)[] audioOutputDevices = []; 60 | private readonly MainViewModel mainViewModel; 61 | private ShowMetadata? projectSettings; 62 | private readonly ObservableCollection nics = []; 63 | private readonly List nicAddresses = []; 64 | 65 | public ProjectSettingsViewModel(MainViewModel mainViewModel) 66 | { 67 | this.mainViewModel = mainViewModel; 68 | AudioOutputDriverValues ??= new(Enum.GetValues()); 69 | PropertyChanged += (o, e) => 70 | { 71 | switch (e.PropertyName) 72 | { 73 | case nameof(AudioLatency): 74 | case nameof(SelectedAudioOutputDeviceIndex): 75 | if (SelectedAudioOutputDeviceIndex >= 0 && SelectedAudioOutputDeviceIndex < audioOutputDevices.Length) 76 | mainViewModel.OpenAudioDevice(); 77 | break; 78 | case nameof(SelectedNIC): 79 | case nameof(OSCRXPort): 80 | case nameof(OSCTXPort): 81 | mainViewModel.ConnectOSC(); 82 | break; 83 | case nameof(MonitorOSCMessages): 84 | mainViewModel.MonitorOSC(MonitorOSCMessages); 85 | break; 86 | } 87 | }; 88 | 89 | _ = AudioOutputDevices; // Update the list of audio devices... 90 | //if (SelectedAudioOutputDeviceIndex < audioOutputDevices.Length) 91 | // mainViewModel.OpenAudioDevice(); 92 | 93 | QueryNICs(); 94 | } 95 | 96 | public static ProjectSettingsViewModel FromModel(ShowMetadata model, MainViewModel mainViewModel) 97 | { 98 | ProjectSettingsViewModel ret = new(mainViewModel); 99 | ret.Title = model.title; 100 | ret.Description = model.description; 101 | ret.Author = model.author; 102 | ret.Date = model.date; 103 | 104 | ret.AudioLatency = model.audioLatency; 105 | ret.AudioOutputDriver = model.audioOutputDriver; 106 | ret.SelectedAudioOutputDeviceIndex = ret.AudioOutputDevices.IndexOf(model.audioOutputDevice); 107 | 108 | ret.OSCRXPort = model.oscRXPort; 109 | ret.OSCTXPort = model.oscTXPort; 110 | if (IPAddress.TryParse(model.oscNIC, out var oscIP)) 111 | ret.SelectedNIC = ret.nicAddresses.IndexOf(oscIP); 112 | 113 | return ret; 114 | } 115 | 116 | public void Bind(ShowMetadata model) 117 | { 118 | projectSettings = model; 119 | PropertyChanged += (o, e) => 120 | { 121 | ProjectSettingsViewModel vm = (ProjectSettingsViewModel)(o ?? throw new NullReferenceException(nameof(ProjectSettingsViewModel))); 122 | if (e.PropertyName != null) 123 | vm.ToModel(e.PropertyName); 124 | }; 125 | } 126 | 127 | public void ToModel(ShowMetadata model) 128 | { 129 | model.title = Title; 130 | model.description = Description; 131 | model.author = Author; 132 | model.date = Date; 133 | 134 | model.audioLatency = AudioLatency; 135 | model.audioOutputDriver = AudioOutputDriver; 136 | var devices = AudioOutputDevices; 137 | model.audioOutputDevice = devices[Math.Clamp(SelectedAudioOutputDeviceIndex, 0, devices.Count)]; 138 | 139 | model.oscRXPort = OSCRXPort; 140 | model.oscTXPort = OSCTXPort; 141 | model.oscNIC = OSCNic.ToString(); 142 | } 143 | 144 | public void ToModel(string propertyName) 145 | { 146 | if (projectSettings == null) 147 | return; 148 | switch (propertyName) 149 | { 150 | case nameof(Title): 151 | projectSettings.title = Title; 152 | break; 153 | case nameof(Description): 154 | projectSettings.description = Description; 155 | break; 156 | case nameof(Author): 157 | projectSettings.author = Author; 158 | break; 159 | case nameof(Date): 160 | projectSettings.date = Date; 161 | break; 162 | case nameof(AudioLatency): 163 | projectSettings.audioLatency = AudioLatency; 164 | break; 165 | case nameof(AudioOutputDriver): 166 | projectSettings.audioOutputDriver = AudioOutputDriver; 167 | break; 168 | case nameof(SelectedAudioOutputDeviceIndex): 169 | var outputDevices = AudioOutputDevices; 170 | projectSettings.audioOutputDevice = outputDevices[Math.Clamp(SelectedAudioOutputDeviceIndex, 0, outputDevices.Count - 1)]; 171 | break; 172 | case nameof(AudioOutputDevices): 173 | case nameof(AudioOutputDriverValues): 174 | break; 175 | case nameof(OSCRXPort): 176 | projectSettings.oscRXPort = OSCRXPort; 177 | break; 178 | case nameof(OSCTXPort): 179 | projectSettings.oscTXPort = OSCTXPort; 180 | break; 181 | case nameof(SelectedNIC): 182 | projectSettings.oscNIC = OSCNic.ToString(); 183 | break; 184 | case nameof(NICs): 185 | case nameof(MonitorOSCMessages): 186 | case nameof(OSCNic): 187 | case nameof(SelectedAudioOutputDeviceKey): 188 | break; 189 | default: 190 | throw new ArgumentException($"Couldn't convert property {propertyName} to model!", nameof(propertyName)); 191 | } 192 | } 193 | 194 | private void QueryNICs() 195 | { 196 | nics.Clear(); 197 | nicAddresses.Clear(); 198 | foreach (var nic in NetworkInterface.GetAllNetworkInterfaces()) 199 | { 200 | var ipProps = nic.GetIPProperties(); 201 | var nicAddr = ipProps.UnicastAddresses.Select(x => x.Address); 202 | if (nicAddr.FirstOrDefault(x => x.AddressFamily == AddressFamily.InterNetwork) is IPAddress naddr 203 | && ipProps.GatewayAddresses.Count >= 0) 204 | { 205 | nicAddresses.AddRange(nicAddr); 206 | foreach (var addr in nicAddr) 207 | nics.Add($"{nic.Name}: {addr}"); 208 | //Log($"\t{nic.Name}: {string.Join(", ", nicAddr)}"); 209 | } 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /QPlayer/ViewModels/StopCueViewModel.cs: -------------------------------------------------------------------------------- 1 | using QPlayer.Models; 2 | using ReactiveUI.Fody.Helpers; 3 | using System; 4 | using System.Linq; 5 | using System.Timers; 6 | using Cue = QPlayer.Models.Cue; 7 | using QPlayer.Audio; 8 | 9 | namespace QPlayer.ViewModels 10 | { 11 | public class StopCueViewModel : CueViewModel, IConvertibleModel 12 | { 13 | [Reactive, ReactiveDependency(nameof(FadeOutTime))] 14 | public override TimeSpan Duration => TimeSpan.FromSeconds(FadeOutTime); 15 | [Reactive] public decimal StopTarget { get; set; } 16 | [Reactive] public StopMode StopMode { get; set; } 17 | [Reactive] public float FadeOutTime { get; set; } 18 | [Reactive] public FadeType FadeType { get; set; } 19 | 20 | private readonly Timer playbackProgressUpdater; 21 | private DateTime startTime; 22 | 23 | public StopCueViewModel(MainViewModel mainViewModel) : base(mainViewModel) 24 | { 25 | playbackProgressUpdater = new Timer 26 | { 27 | AutoReset = true, 28 | Interval = 100 29 | }; 30 | playbackProgressUpdater.Elapsed += PlaybackProgressUpdater_Elapsed; 31 | PropertyChanged += (o, e) => 32 | { 33 | switch (e.PropertyName) 34 | { 35 | case nameof(FadeOutTime): 36 | OnPropertyChanged(nameof(Duration)); 37 | OnPropertyChanged(nameof(PlaybackTimeString)); 38 | OnPropertyChanged(nameof(PlaybackTimeStringShort)); 39 | break; 40 | } 41 | }; 42 | } 43 | 44 | private void PlaybackProgressUpdater_Elapsed(object? sender, ElapsedEventArgs e) 45 | { 46 | PlaybackTime = DateTime.Now.Subtract(startTime); 47 | if (PlaybackTime >= Duration) 48 | { 49 | synchronizationContext?.Post(x => Stop(), null); 50 | } 51 | } 52 | 53 | public override void Go() 54 | { 55 | base.Go(); 56 | // Stop cues don't support preloading 57 | PlaybackTime = TimeSpan.Zero; 58 | startTime = DateTime.Now; 59 | playbackProgressUpdater.Start(); 60 | var cue = mainViewModel?.Cues.FirstOrDefault(x => x.QID == StopTarget); 61 | if(cue != null) 62 | { 63 | if (cue is SoundCueViewModel soundCue) 64 | soundCue.FadeOutAndStop(FadeOutTime, FadeType); 65 | else 66 | { 67 | cue.Stop(); 68 | Stop(); 69 | } 70 | } else 71 | { 72 | Stop(); 73 | } 74 | } 75 | 76 | public override void Stop() 77 | { 78 | base.Stop(); 79 | playbackProgressUpdater.Stop(); 80 | PlaybackTime = TimeSpan.Zero; 81 | } 82 | 83 | public override void Pause() 84 | { 85 | // Pausing isn't supported on stop cues 86 | //base.Pause(); 87 | Stop(); 88 | } 89 | 90 | public override void ToModel(string propertyName) 91 | { 92 | base.ToModel(propertyName); 93 | if (cueModel is StopCue scue) 94 | { 95 | switch (propertyName) 96 | { 97 | case nameof(StopTarget): scue.stopQid = StopTarget; break; 98 | case nameof(StopMode): scue.stopMode = StopMode; break; 99 | case nameof(FadeOutTime): scue.fadeOutTime = FadeOutTime; break; 100 | case nameof(FadeType): scue.fadeType = FadeType; break; 101 | } 102 | } 103 | } 104 | 105 | public override void ToModel(Cue cue) 106 | { 107 | base.ToModel(cue); 108 | if (cue is StopCue scue) 109 | { 110 | scue.stopQid = StopTarget; 111 | scue.stopMode = StopMode; 112 | scue.fadeOutTime = FadeOutTime; 113 | scue.fadeType = FadeType; 114 | } 115 | } 116 | 117 | public static new CueViewModel FromModel(Cue cue, MainViewModel mainViewModel) 118 | { 119 | StopCueViewModel vm = new(mainViewModel); 120 | if (cue is StopCue scue) 121 | { 122 | vm.StopTarget = scue.stopQid; 123 | vm.StopMode = scue.stopMode; 124 | vm.FadeOutTime = scue.fadeOutTime; 125 | vm.FadeType = scue.fadeType; 126 | } 127 | return vm; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /QPlayer/ViewModels/TimeCodeCueViewModel.cs: -------------------------------------------------------------------------------- 1 | using QPlayer.Models; 2 | using ReactiveUI.Fody.Helpers; 3 | using System; 4 | using Cue = QPlayer.Models.Cue; 5 | 6 | namespace QPlayer.ViewModels 7 | { 8 | public class TimeCodeCueViewModel : CueViewModel, IConvertibleModel 9 | { 10 | [Reactive] public TimeSpan StartTime { get; set; } 11 | [Reactive, ReactiveDependency(nameof(TCDuration))] 12 | public override TimeSpan Duration => TCDuration; 13 | [Reactive] public TimeSpan TCDuration { get; set; } 14 | 15 | public TimeCodeCueViewModel(MainViewModel mainViewModel) : base(mainViewModel) 16 | { 17 | } 18 | 19 | public override void ToModel(string propertyName) 20 | { 21 | base.ToModel(propertyName); 22 | if (cueModel is TimeCodeCue tccue) 23 | { 24 | switch (propertyName) 25 | { 26 | case nameof(StartTime): tccue.startTime = StartTime; break; 27 | case nameof(TCDuration): tccue.duration = TCDuration; break; 28 | } 29 | } 30 | } 31 | 32 | public override void ToModel(Cue cue) 33 | { 34 | base.ToModel(cue); 35 | if (cue is TimeCodeCue tccue) 36 | { 37 | tccue.startTime = StartTime; 38 | tccue.duration = TCDuration; 39 | } 40 | } 41 | 42 | public static new CueViewModel FromModel(Cue cue, MainViewModel mainViewModel) 43 | { 44 | TimeCodeCueViewModel vm = new(mainViewModel); 45 | if (cue is TimeCodeCue tccue) 46 | { 47 | vm.StartTime = tccue.startTime; 48 | vm.TCDuration = tccue.duration; 49 | } 50 | return vm; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /QPlayer/ViewModels/VolumeCueViewModel.cs: -------------------------------------------------------------------------------- 1 | using QPlayer.Models; 2 | using ReactiveUI.Fody.Helpers; 3 | using System; 4 | using System.Linq; 5 | using System.Timers; 6 | using Cue = QPlayer.Models.Cue; 7 | using QPlayer.Audio; 8 | 9 | namespace QPlayer.ViewModels 10 | { 11 | public class VolumeCueViewModel : CueViewModel, IConvertibleModel 12 | { 13 | [Reactive, ReactiveDependency(nameof(FadeTime))] 14 | public override TimeSpan Duration => TimeSpan.FromSeconds(FadeTime); 15 | [Reactive] public decimal Target { get; set; } 16 | [Reactive] public float Volume { get; set; } 17 | [Reactive] public float FadeTime { get; set; } 18 | [Reactive] public FadeType FadeType { get; set; } 19 | 20 | private readonly Timer playbackProgressUpdater; 21 | private DateTime startTime; 22 | 23 | public VolumeCueViewModel(MainViewModel mainViewModel) : base(mainViewModel) 24 | { 25 | playbackProgressUpdater = new Timer 26 | { 27 | AutoReset = true, 28 | Interval = 100 29 | }; 30 | playbackProgressUpdater.Elapsed += PlaybackProgressUpdater_Elapsed; 31 | PropertyChanged += (o, e) => 32 | { 33 | switch (e.PropertyName) 34 | { 35 | case nameof(FadeTime): 36 | OnPropertyChanged(nameof(Duration)); 37 | OnPropertyChanged(nameof(PlaybackTimeString)); 38 | OnPropertyChanged(nameof(PlaybackTimeStringShort)); 39 | break; 40 | } 41 | }; 42 | } 43 | 44 | private void PlaybackProgressUpdater_Elapsed(object? sender, ElapsedEventArgs e) 45 | { 46 | PlaybackTime = DateTime.Now.Subtract(startTime); 47 | if (PlaybackTime >= Duration) 48 | { 49 | synchronizationContext?.Post(x => Stop(), null); 50 | } 51 | } 52 | 53 | public override void Go() 54 | { 55 | base.Go(); 56 | // Volume cues don't support preloading 57 | PlaybackTime = TimeSpan.Zero; 58 | startTime = DateTime.Now; 59 | playbackProgressUpdater.Start(); 60 | var cue = mainViewModel?.Cues.FirstOrDefault(x => x.QID == Target); 61 | if(cue != null) 62 | { 63 | if (cue is SoundCueViewModel soundCue) 64 | soundCue.Fade(Volume, FadeTime, FadeType); 65 | else 66 | Stop(); 67 | } else 68 | { 69 | Stop(); 70 | } 71 | } 72 | 73 | public override void Stop() 74 | { 75 | base.Stop(); 76 | playbackProgressUpdater.Stop(); 77 | PlaybackTime = TimeSpan.Zero; 78 | } 79 | 80 | public override void Pause() 81 | { 82 | // Pausing isn't supported on stop cues 83 | //base.Pause(); 84 | Stop(); 85 | } 86 | 87 | public override void ToModel(string propertyName) 88 | { 89 | base.ToModel(propertyName); 90 | if (cueModel is VolumeCue scue) 91 | { 92 | switch (propertyName) 93 | { 94 | case nameof(Target): scue.soundQid = Target; break; 95 | case nameof(Volume): scue.volume = Volume; break; 96 | case nameof(FadeTime): scue.fadeTime = FadeTime; break; 97 | case nameof(FadeType): scue.fadeType = FadeType; break; 98 | } 99 | } 100 | } 101 | 102 | public override void ToModel(Cue cue) 103 | { 104 | base.ToModel(cue); 105 | if (cue is VolumeCue scue) 106 | { 107 | scue.soundQid = Target; 108 | scue.volume = Volume; 109 | scue.fadeTime = FadeTime; 110 | scue.fadeType = FadeType; 111 | } 112 | } 113 | 114 | public static new CueViewModel FromModel(Cue cue, MainViewModel mainViewModel) 115 | { 116 | VolumeCueViewModel vm = new(mainViewModel); 117 | if (cue is VolumeCue scue) 118 | { 119 | vm.Target = scue.soundQid; 120 | vm.Volume = scue.volume; 121 | vm.FadeTime = scue.fadeTime; 122 | vm.FadeType = scue.fadeType; 123 | } 124 | return vm; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /QPlayer/ViewModels/WaveFormRenderer.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using DynamicData; 3 | using NAudio.Wave; 4 | using ReactiveUI.Fody.Helpers; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.IO.Compression; 10 | using System.Linq; 11 | using System.Reflection; 12 | using System.Runtime.InteropServices; 13 | using System.Text; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | using System.Windows; 17 | using System.Windows.Media; 18 | using QPlayer.Models; 19 | 20 | namespace QPlayer.ViewModels; 21 | 22 | public class WaveFormRenderer : ObservableObject 23 | { 24 | [Reactive] 25 | public PeakFile? PeakFile 26 | { 27 | set 28 | { 29 | peakFile = value; 30 | startTime = 0; 31 | endTime = 1; 32 | InternalUpdate(); 33 | } 34 | get => peakFile; 35 | } 36 | [Reactive] public SoundCueViewModel SoundCueViewModel { get; init; } 37 | [Reactive] public Drawing WaveFormDrawing => drawingGroup; 38 | [Reactive] 39 | public double Width 40 | { 41 | get => width; 42 | set { var prev = width; width = (int)value; if (prev != value) InternalUpdate(); } 43 | } 44 | [Reactive] 45 | public double Height 46 | { 47 | get => height; 48 | set { var prev = height; height = (int)value; if (prev != value) InternalUpdate(); } 49 | } 50 | [Reactive] 51 | public TimeSpan ViewStart 52 | { 53 | get => TimeSpan.FromSeconds(startTime * (peakFile?.length ?? 0) / (double)(peakFile?.fs ?? 1)); 54 | set { startTime = Math.Clamp(((float)value.TotalSeconds) / ((peakFile?.length ?? 0) / (float)(peakFile?.fs ?? 1)), 0, 1); InternalUpdate(); } 55 | } 56 | [Reactive] 57 | public TimeSpan ViewEnd 58 | { 59 | get => TimeSpan.FromSeconds(endTime * (peakFile?.length ?? 0) / (double)(peakFile?.fs ?? 1)); 60 | set { endTime = Math.Clamp(((float)value.TotalSeconds) / ((peakFile?.length ?? 0) / (float)(peakFile?.fs ?? 1)), 0, 1); InternalUpdate(); } 61 | } 62 | [Reactive] 63 | public TimeSpan ViewSpan => TimeSpan.FromSeconds((endTime - startTime) * (peakFile?.length ?? 0) / (double)(peakFile?.fs ?? 1)); 64 | [Reactive] 65 | public TimeSpan Duration => TimeSpan.FromSeconds((peakFile?.length ?? 0) / (double)(peakFile?.fs ?? 1)); 66 | [Reactive, ReactiveDependency(nameof(PeakFile))] public string FileName => peakFile?.sourceName ?? string.Empty; 67 | [Reactive, ReactiveDependency(nameof(PeakFile))] public string WindowTitle => $"QPlayer - Waveform - {peakFile?.sourceName ?? string.Empty}"; 68 | 69 | /// 70 | /// This is the name of a special property change notification which the view listens to so that it knows to 71 | /// propagate the correct width/height/cursors back to the renderer. 72 | /// 73 | internal const string OnVMUpdate = "OnVMUpdate"; 74 | 75 | // Accursed over-abstraction... 76 | private readonly Brush peakBrush = new SolidColorBrush(Color.FromArgb(200, 10, 90, 255)); 77 | private readonly Pen peakPen; 78 | private readonly Brush rmsBrush = new SolidColorBrush(Color.FromArgb(200, 10, 30, 220)); 79 | private readonly Pen rmsPen; 80 | private readonly DrawingGroup drawingGroup; 81 | private readonly GeometryDrawing geometryDrawingPeak; 82 | private readonly PathGeometry geometryPeak; 83 | private readonly PathFigure figurePeak; 84 | private readonly PolyLineSegment peakPoly; 85 | private readonly GeometryDrawing geometryDrawingRMS; 86 | private readonly PathGeometry geometryRMS; 87 | private readonly PathFigure figureRMS; 88 | private readonly PolyLineSegment rmsPoly; 89 | private readonly List peakPoints = []; 90 | private readonly List rmsPoints = []; 91 | private readonly RectangleGeometry clipGeometry; 92 | 93 | // WPF can be slow, especially with complex shapes, so we limit the maximum number of points displayed here; 94 | // This only really has an effect on the waveform popup (due to it's large width). 95 | private const int MAX_DISPLAYED_POINTS = 500; 96 | // Lowering this lowers the amount of detail in the waveforms, which is good for performance. 97 | private const float WAVEFORM_DETAIL_FACTOR = 0.75f; 98 | 99 | private PeakFile? peakFile = null; 100 | private int width = 2; 101 | private int height = 2; 102 | private float startTime = 0; 103 | private float endTime = 1; 104 | 105 | public WaveFormRenderer(SoundCueViewModel soundCue) 106 | { 107 | // Accursed over-abstraction... Blame Microsoft... 108 | peakPen = new(peakBrush, 0); 109 | figurePeak = new() 110 | { 111 | IsClosed = true, 112 | IsFilled = true 113 | }; 114 | peakPoly = new PolyLineSegment(); 115 | figurePeak.Segments.Add(peakPoly); 116 | 117 | geometryPeak = new(); 118 | geometryPeak.Figures.Add(figurePeak); 119 | geometryDrawingPeak = new GeometryDrawing(peakBrush, peakPen, geometryPeak); 120 | 121 | rmsPen = new(rmsBrush, 0); 122 | figureRMS = new() 123 | { 124 | IsClosed = true, 125 | IsFilled = true 126 | }; 127 | rmsPoly = new PolyLineSegment(); 128 | figureRMS.Segments.Add(rmsPoly); 129 | geometryRMS = new(); 130 | geometryRMS.Figures.Add(figureRMS); 131 | 132 | clipGeometry = new(new Rect(0, 0, width, height)); 133 | 134 | geometryDrawingRMS = new GeometryDrawing(rmsBrush, rmsPen, geometryRMS); 135 | drawingGroup = new(); 136 | drawingGroup.Children.Add(geometryDrawingPeak); 137 | drawingGroup.Children.Add(geometryDrawingRMS); 138 | drawingGroup.ClipGeometry = clipGeometry; 139 | 140 | SoundCueViewModel = soundCue; 141 | 142 | //WaveFormDrawing = new DrawingImage(drawingGroup); 143 | } 144 | 145 | private void InternalUpdate() 146 | { 147 | OnPropertyChanged(nameof(ViewSpan)); 148 | OnPropertyChanged(nameof(Duration)); 149 | Render(); 150 | } 151 | 152 | public void Update() 153 | { 154 | OnPropertyChanged(OnVMUpdate); 155 | //InternalUpdate(); 156 | } 157 | 158 | private void Render() 159 | { 160 | if (peakFile == null) 161 | return; 162 | 163 | if (width != clipGeometry.Rect.Width || height != clipGeometry.Rect.Height) 164 | { 165 | clipGeometry.Rect = new Rect(0, 0, width, height); 166 | } 167 | 168 | peakPoints.Clear(); 169 | rmsPoints.Clear(); 170 | figurePeak.StartPoint = new Point(0, height); 171 | figureRMS.StartPoint = new Point(0, height); 172 | //peakPoints.Add(new Point(0, height)); 173 | //rmsPoints.Add(new Point(0, height)); 174 | 175 | float viewSpan = endTime - startTime; 176 | // Find a pyramid with slightly fewer points than the width in pixels of the final waveform. 177 | float targetLength = Math.Min(width * WAVEFORM_DETAIL_FACTOR, MAX_DISPLAYED_POINTS) / viewSpan; 178 | var buff = peakFile.Value.peakDataPyramid.LastOrDefault( 179 | x => x.samples.Length <= targetLength, 180 | peakFile.Value.peakDataPyramid[0]); 181 | // A value between 0-1 where 1 indicates that we are close to using the full resolution of the pyramid, and 0 means we are close to the lower resolution 182 | float lodLerp = MathF.Max(0, (buff.samples.Length * 2 - targetLength) / targetLength); 183 | int sampleStart = Math.Clamp((int)(buff.samples.Length * startTime), 0, buff.samples.Length - 1); 184 | int sampleEnd = Math.Clamp((int)(buff.samples.Length * endTime), 0, buff.samples.Length - 1); 185 | int samples = sampleEnd - sampleStart; 186 | 187 | int j = 0; 188 | float prevPeak = 0; 189 | float prevRms = 0; 190 | for (int i = sampleStart; i < sampleEnd + 1; i++) 191 | { 192 | float x = (j * width) / (float)samples; 193 | float peakVal = buff.samples[i].peak / (float)ushort.MaxValue; 194 | float rmsVal = buff.samples[i].rms / (float)ushort.MaxValue; 195 | 196 | float peakLerped = Lerp(peakVal, MathF.Max(peakVal, prevPeak), lodLerp); 197 | float rmsLerped = Lerp(rmsVal, (rmsVal + prevRms) * .5f, lodLerp); 198 | prevPeak = peakVal; 199 | prevRms = rmsVal; 200 | 201 | float peak = height - MathF.Sqrt(peakLerped) * height; 202 | float rms = height - MathF.Sqrt(rmsLerped) * height; 203 | 204 | peakPoints.Add(new Point(x, peak)); 205 | rmsPoints.Add(new Point(x, rms)); 206 | j++; 207 | } 208 | 209 | peakPoints.Add(new Point(width, height)); 210 | rmsPoints.Add(new Point(width, height)); 211 | 212 | peakPoly.Points.Clear(); 213 | rmsPoly.Points.Clear(); 214 | peakPoly.Points.Add(peakPoints); 215 | rmsPoly.Points.Add(rmsPoints); 216 | 217 | OnPropertyChanged(nameof(WaveFormDrawing)); 218 | } 219 | 220 | private static float Lerp(float a, float b, float t) 221 | { 222 | return b * t + a * (1 - t); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /QPlayer/Views/AboutWindow.xaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /QPlayer/Views/AboutWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Data; 10 | using System.Windows.Documents; 11 | using System.Windows.Input; 12 | using System.Windows.Media; 13 | using System.Windows.Media.Imaging; 14 | using System.Windows.Shapes; 15 | using QPlayer.ViewModels; 16 | 17 | namespace QPlayer.Views 18 | { 19 | /// 20 | /// Interaction logic for AboutWindow.xaml 21 | /// 22 | public partial class AboutWindow : Window 23 | { 24 | public AboutWindow(MainViewModel viewModel) 25 | { 26 | this.DataContext = viewModel; 27 | InitializeComponent(); 28 | } 29 | 30 | private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) 31 | { 32 | Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute=true }); 33 | e.Handled = true; 34 | } 35 | 36 | private void Image_MouseDown(object sender, MouseButtonEventArgs e) 37 | { 38 | Process.Start(new ProcessStartInfo("https://github.com/space928/QPlayer/") { UseShellExecute = true }); 39 | e.Handled = true; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /QPlayer/Views/ActiveCueControl.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 36 | 37 | -------------------------------------------------------------------------------- /QPlayer/Views/ActiveCueControl.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Data; 10 | using System.Windows.Documents; 11 | using System.Windows.Input; 12 | using System.Windows.Media; 13 | using System.Windows.Media.Imaging; 14 | using System.Windows.Navigation; 15 | using System.Windows.Shapes; 16 | 17 | namespace QPlayer.Views 18 | { 19 | /// 20 | /// Interaction logic for ActiveCueControl.xaml 21 | /// 22 | public partial class ActiveCueControl : UserControl 23 | { 24 | public ActiveCueControl() 25 | { 26 | InitializeComponent(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /QPlayer/Views/CueDataControl.xaml: -------------------------------------------------------------------------------- 1 |  11 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 61 | 62 | -------------------------------------------------------------------------------- /QPlayer/Views/CueDataControl.xaml.cs: -------------------------------------------------------------------------------- 1 | using QPlayer.ViewModels; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using System.Windows; 10 | using System.Windows.Controls; 11 | using System.Windows.Data; 12 | using System.Windows.Documents; 13 | using System.Windows.Input; 14 | using System.Windows.Media; 15 | using System.Windows.Media.Imaging; 16 | using System.Windows.Navigation; 17 | using System.Windows.Shapes; 18 | 19 | namespace QPlayer.Views; 20 | 21 | /// 22 | /// Interaction logic for CueDataControl.xaml 23 | /// 24 | public partial class CueDataControl : UserControl 25 | { 26 | const int DragDeadzone = 10; 27 | 28 | private Point startPos; 29 | 30 | public CueDataControl() 31 | { 32 | InitializeComponent(); 33 | //this.DataContext = this; 34 | } 35 | 36 | private void Grid_MouseDown(object sender, MouseButtonEventArgs e) 37 | { 38 | startPos = e.GetPosition(this); 39 | 40 | var vm = (CueViewModel)DataContext; 41 | if (vm.MainViewModel != null) 42 | Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, () => vm.SelectCommand.Execute(null)); 43 | } 44 | 45 | private void UserControl_Loaded(object sender, RoutedEventArgs e) 46 | { 47 | var vm = (CueViewModel)DataContext; 48 | vm.PropertyChanged += (o, e) => 49 | { 50 | switch (e.PropertyName) 51 | { 52 | case nameof(CueViewModel.IsSelected): 53 | if (vm.IsSelected) 54 | { 55 | // This is a lazy way to check if the last action that selected us was a click or some other kind of Go() 56 | // If the user clicks on the element we shouldn't risk it moving too much 57 | if (IsMouseOver) 58 | BringIntoView(); 59 | else 60 | BringIntoView(new Rect(new Size(10, 200))); // Leave some padding below us 61 | } 62 | break; 63 | } 64 | }; 65 | } 66 | 67 | private void Grid_PreviewMouseDown(object sender, MouseButtonEventArgs e) 68 | { 69 | Focus(); 70 | } 71 | 72 | private void Grid_MouseMove(object sender, MouseEventArgs e) 73 | { 74 | base.OnMouseMove(e); 75 | 76 | var delta = e.GetPosition(this) - startPos; 77 | var vm = (CueViewModel)DataContext; 78 | if (e.LeftButton == MouseButtonState.Pressed && delta.Length > DragDeadzone 79 | && (vm.MainViewModel?.DraggingCues?.Count ?? -1) == 0 80 | && !e.OriginalSource.GetType().IsAssignableTo(typeof(TextBox))) 81 | { 82 | DataObject data = new(); 83 | vm.MainViewModel?.DraggingCues?.Add(vm); 84 | data.SetData("Cues", new CueViewModel[] { vm }); 85 | 86 | DragDrop.DoDragDrop(this, data, DragDropEffects.Move); 87 | 88 | vm.MainViewModel?.DraggingCues?.Clear(); 89 | } 90 | } 91 | 92 | private void Grid_GiveFeedback(object sender, GiveFeedbackEventArgs e) 93 | { 94 | base.OnGiveFeedback(e); 95 | 96 | if (e.Effects.HasFlag(DragDropEffects.Copy)) 97 | Mouse.SetCursor(Cursors.Cross); 98 | else if (e.Effects.HasFlag(DragDropEffects.Move)) 99 | Mouse.SetCursor(Cursors.Hand); 100 | else 101 | Mouse.SetCursor(Cursors.No); 102 | 103 | e.Handled = true; 104 | } 105 | 106 | private void Grid_Drop(object sender, DragEventArgs e) 107 | { 108 | base.OnDrop(e); 109 | 110 | InsertMarker.Visibility = Visibility.Hidden; 111 | 112 | var targetVm = (CueViewModel)DataContext; 113 | var mainVm = targetVm.MainViewModel; 114 | if (mainVm != null) 115 | MainWindow.HandleCueListDrop(e, mainVm, targetVm); 116 | 117 | e.Handled = true; 118 | } 119 | 120 | private void Grid_DragEnter(object sender, DragEventArgs e) 121 | { 122 | InsertMarker.Visibility = Visibility.Visible; 123 | } 124 | 125 | private void Grid_DragLeave(object sender, DragEventArgs e) 126 | { 127 | InsertMarker.Visibility = Visibility.Hidden; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /QPlayer/Views/CueDataHeaderControl.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 50 | 51 | -------------------------------------------------------------------------------- /QPlayer/Views/CueDataHeaderControl.xaml.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.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace QPlayer.Views 17 | { 18 | /// 19 | /// Interaction logic for CueDataHeaderControl.xaml 20 | /// 21 | public partial class CueDataHeaderControl : UserControl 22 | { 23 | public CueDataHeaderControl() 24 | { 25 | InitializeComponent(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /QPlayer/Views/CueDataTemplates.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /QPlayer/Views/CueDataTemplates.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace QPlayer.Views 8 | { 9 | public partial class CueDataTemplates 10 | { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /QPlayer/Views/CueEditor.xaml.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.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace QPlayer.Views; 17 | 18 | /// 19 | /// Interaction logic for CueEditor.xaml 20 | /// 21 | public partial class CueEditor : UserControl 22 | { 23 | public CueEditor() 24 | { 25 | InitializeComponent(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /QPlayer/Views/HiddenTextbox.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /QPlayer/Views/HiddenTextbox.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Data; 4 | using System.Windows.Input; 5 | 6 | namespace QPlayer.Views; 7 | 8 | /// 9 | /// Interaction logic for HiddenTextbox.xaml 10 | /// 11 | public partial class HiddenTextbox : UserControl 12 | { 13 | private bool editing = false; 14 | 15 | public bool IsEditing => editing; 16 | 17 | public HiddenTextbox() 18 | { 19 | InitializeComponent(); 20 | } 21 | 22 | public string Text 23 | { 24 | get => (string)GetValue(TextProperty); 25 | set => SetValue(TextProperty, value); 26 | } 27 | 28 | public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(HiddenTextbox), new FrameworkPropertyMetadata 29 | { 30 | BindsTwoWayByDefault = true, 31 | DefaultUpdateSourceTrigger = UpdateSourceTrigger.LostFocus 32 | }); 33 | 34 | private void Label_MouseDoubleClick(object sender, MouseButtonEventArgs e) 35 | { 36 | editing = true; 37 | TextFieldInst.Visibility = Visibility.Visible; 38 | Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, TextFieldInst.TextBox.Focus); 39 | } 40 | 41 | private void Label_MouseDown(object sender, MouseButtonEventArgs e) 42 | { 43 | 44 | } 45 | 46 | private void TextBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) 47 | { 48 | editing = false; 49 | TextFieldInst.Visibility = Visibility.Collapsed; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /QPlayer/Views/LibraryImports.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace QPlayer.Views; 9 | 10 | public partial class LibraryImports 11 | { 12 | [StructLayout(LayoutKind.Sequential)] 13 | public struct POINT(int x, int y) 14 | { 15 | public int x = x; 16 | public int y = y; 17 | 18 | //public POINT() : this(0,0) { } 19 | } 20 | 21 | [LibraryImport("user32.dll", EntryPoint = "SetCursorPos")] 22 | public static partial long SetCursorPos(int x, int y); 23 | 24 | [LibraryImport("user32.dll", EntryPoint = "GetCursorPos", SetLastError = true)] 25 | [return: MarshalAs(UnmanagedType.Bool)] 26 | public static partial bool GetCursorPos(out POINT lpPoint); 27 | 28 | /// 29 | /// Displays or hides the cursor. 30 | /// 31 | /// 32 | /// If bShow is TRUE, the display count is incremented by one. If bShow is FALSE, the display count is decremented by one. 33 | /// 34 | /// The return value specifies the new display counter. 35 | [LibraryImport("user32.dll", EntryPoint = "ShowCursor")] 36 | public static partial int ShowCursor([MarshalAs(UnmanagedType.Bool)] bool bShow); 37 | } 38 | -------------------------------------------------------------------------------- /QPlayer/Views/LogWindow.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 15 | 16 | 17 | 18 | 19 | 29 | 32 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /QPlayer/Views/WaveFormWindow.xaml.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.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Shapes; 14 | 15 | namespace QPlayer.Views 16 | { 17 | /// 18 | /// Interaction logic for WaveFormWindow.xaml 19 | /// 20 | public partial class WaveFormWindow : Window 21 | { 22 | public WaveFormWindow() 23 | { 24 | InitializeComponent(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QPlayer 2 |

3 | QPlayer Logo 4 |

5 | 6 | [![Build Status](https://github.com/space928/QPlayer/actions/workflows/dotnet-desktop.yml/badge.svg?branch=main)](https://github.com/space928/QPlayer/actions/workflows/dotnet-desktop.yml) 7 | [![GitHub License](https://img.shields.io/github/license/space928/QPlayer)](https://github.com/space928/QPlayer/blob/main/LICENSE) 8 | [![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/space928/QPlayer/total)](https://github.com/space928/QPlayer/releases) 9 | [![Documentation](https://img.shields.io/badge/Documentation-darkgreen?color=%2324aa2a&link=https%3A%2F%2Fspace928.github.io%2FQPlayer)](https://space928.github.io/QPlayer) 10 | 11 | 12 | QPlayer is a simple media player for theatre. It allows cue lists of sound tracks to be created and 13 | played. Media playback is handled by NAudio, providing a large range of supported media. 14 | 15 | **Features:** 16 | - Playback of a range of audio types (wav, mp3, etc...) 17 | - Playback of multiple cues concurrently 18 | - Fade in and fade out 19 | - Pausing and preloading cues 20 | - Cue pre-delays 21 | 22 | 23 | ![Application screenshot](https://github.com/space928/QPlayer/assets/15130114/1a63eaaa-2c13-48e4-be0e-e33b5921bb41) 24 | 25 | ## Building 26 | QPlayer can be built with Visual Studio 2022 using the .NET SDK 7. 27 | 28 | Only Windows is officially supported for now. 29 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /docs/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /docs/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /docs/QPlayerDocs.esproj: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # QPlayer Documentation 2 | 3 | This project contains the source files used to build the documentation for QPlayer. 4 | 5 | [![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) 6 | 7 | ## Project Structure 8 | 9 | ``` 10 | . 11 | ├── public/ 12 | ├── src/ 13 | │ ├── assets/ 14 | │ ├── content/ 15 | │ │ ├── docs/ 16 | │ └── content.config.ts 17 | ├── astro.config.mjs 18 | ├── package.json 19 | └── tsconfig.json 20 | ``` 21 | 22 | Starlight (the documentation build system) looks for `.md` or `.mdx` files in the 23 | `src/content/docs/` directory. Each file is exposed as a route based on its file 24 | name. 25 | 26 | Images can be added to `src/assets/` and embedded in Markdown with a relative link. 27 | 28 | Static assets, like favicons, can be placed in the `public/` directory. 29 | 30 | ## Commands 31 | 32 | All commands are run from the root of the project, from a terminal: 33 | 34 | | Command | Action | 35 | | :------------------------ | :----------------------------------------------- | 36 | | `npm install` | Installs dependencies | 37 | | `npm run dev` | Starts local dev server at `localhost:4321` | 38 | | `npm run build` | Build the documentation site to `./dist/` | 39 | | `npm run preview` | Preview the built docs locally, before deploying | 40 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 41 | | `npm run astro -- --help` | Get help using the Astro CLI | 42 | 43 | Documentation for the build system: 44 | - [Starlight](https://starlight.astro.build/) 45 | - [Astro](https://docs.astro.build) 46 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig } from 'astro/config'; 3 | import starlight from '@astrojs/starlight'; 4 | import starlightAutoSidebar from 'starlight-auto-sidebar'; 5 | import starlightGiscus from 'starlight-giscus'; 6 | import starlightKbd from 'starlight-kbd' 7 | 8 | // https://astro.build/config 9 | export default defineConfig({ 10 | site: 'https://space928.github.io', 11 | base: '/QPlayer', 12 | integrations: [ 13 | starlight({ 14 | title: 'QPlayer Documentation', 15 | plugins: [ 16 | starlightAutoSidebar(), 17 | starlightGiscus({ 18 | repo: 'space928/QPlayer', 19 | repoId: 'R_kgDOLSwoIA', 20 | category: 'Comments', 21 | categoryId: 'DIC_kwDOLSwoIM4CobZs', 22 | mapping: '', 23 | reactions: true, 24 | lazy: true 25 | }), 26 | starlightKbd({ 27 | types: [ 28 | { id: 'mac', label: 'macOS' }, 29 | { id: 'windows', label: 'Windows', default: true }, 30 | ], 31 | }), 32 | ], 33 | logo: { 34 | src: './src/assets/Splash.png', 35 | replacesTitle: true, 36 | }, 37 | social: { 38 | github: 'https://github.com/space928/QPlayer', 39 | }, 40 | sidebar: [ 41 | { 42 | label: 'Guides', 43 | items: [ 44 | // Each item here is one entry in the navigation menu. 45 | { label: 'Getting Started', slug: 'guides/getting-started' }, 46 | ], 47 | }, 48 | { 49 | label: 'Reference', 50 | autogenerate: { directory: 'reference' }, 51 | }, 52 | ], 53 | customCss: [ 54 | // Relative path to your custom CSS file 55 | './src/styles/custom.css', 56 | ], 57 | }), 58 | ], 59 | }); 60 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qplayer-docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/starlight": "^0.32.4", 14 | "astro": "^5.5.3", 15 | "sharp": "^0.32.5", 16 | "starlight-auto-sidebar": "^0.1.0", 17 | "starlight-giscus": "^0.5.1", 18 | "starlight-kbd": "^0.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/public/favicon.png -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?><svg id="Logo_Small" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><style>.cls-1{fill:#24aa2a;}.cls-2{fill:#152519;}.cls-3{fill:#7ee6ad;}</style></defs><circle class="cls-2" cx="256" cy="256" r="240"/><path class="cls-3" d="M256.5,85.9998c33.6641,0,62.75,6.75,87.25,20.25s43.25,32.9219,56.25,58.25c13,25.3359,19.5,55.8359,19.5,91.5s-6.5,66.1724-19.5,91.5005c-13,25.3359-31.75,44.75-56.25,58.25s-53.5859,20.25-87.25,20.25-62.75-6.75-87.25-20.25-43.4219-32.9141-56.75-58.25c-13.3359-25.3281-20-55.8281-20-91.5005s6.6641-66.1641,20-91.5c13.3281-25.3281,32.25-44.75,56.75-58.25s53.5781-20.25,87.25-20.25ZM256.5,158.9998c-14.6719,0-27,3.5859-37,10.75-10,7.1719-17.5859,17.9219-22.75,32.25-5.1719,14.3359-7.75,32.3359-7.75,54s2.5781,39.6724,7.75,54.0005c5.1641,14.3359,12.75,25.0859,22.75,32.25,10,7.1719,22.3281,10.75,37,10.75,14.6641,0,26.9141-3.5781,36.75-10.75,9.8281-7.1641,17.25-17.9141,22.25-32.25,5-14.3281,7.5-32.3281,7.5-54.0005s-2.5-39.6641-7.5-54c-5-14.3281-12.4219-25.0781-22.25-32.25-9.8359-7.1641-22.0859-10.75-36.75-10.75Z"/><path class="cls-1" d="M280.4941,487.7617c-16.7969,0-30.4629-13.665-30.4629-30.4619v-183.2119c0-16.7969,13.666-30.4624,30.4629-30.4624,5.3066,0,10.5625,1.4146,15.2002,4.0903l158.7822,91.6055c9.3955,5.4199,15.2324,15.5254,15.2324,26.373s-5.8369,20.9521-15.2324,26.3721l-158.7832,91.6055c-4.6377,2.6758-9.8936,4.0898-15.1982,4.0898h-.001Z"/><path class="cls-2" d="M280.4942,263.6255c1.7382,0,3.5223.4429,5.2048,1.4135l158.7833,91.6056c6.9684,4.0204,6.9684,14.0779,0,18.0983l-158.7833,91.6056c-1.6829.9709-3.4662,1.4135-5.2048,1.4135-5.4565,0-10.4629-4.3648-10.4629-10.4626v-183.2115c0-6.0982,5.0059-10.4626,10.4629-10.4626M280.4942,223.6255c-27.8254,0-50.4629,22.6374-50.4629,50.4625v183.2115c0,27.8251,22.6375,50.4625,50.4629,50.4625,8.8093,0,17.521-2.3396,25.1934-6.766l158.7835-91.6057c15.5673-8.9813,25.2375-25.7246,25.2375-43.6965s-9.6703-34.7152-25.2372-43.6964l-158.7836-91.6057c-7.673-4.4266-16.3846-6.7661-25.1937-6.7661h0Z"/></svg> -------------------------------------------------------------------------------- /docs/src/assets/IconXL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/IconXL.png -------------------------------------------------------------------------------- /docs/src/assets/Logo.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 480"><defs><style>.cls-1{fill:#24aa2a;}.cls-2{fill:#152519;}.cls-3{fill:#7ee6ad;}</style></defs><circle class="cls-2" cx="240" cy="240" r="240"/><path class="cls-3" d="M240.5,69.9998c33.6641,0,62.75,6.75,87.25,20.25s43.25,32.9219,56.25,58.25c13,25.3359,19.5,55.8359,19.5,91.5s-6.5,66.1724-19.5,91.5005c-13,25.3359-31.75,44.75-56.25,58.25s-53.5859,20.25-87.25,20.25-62.75-6.75-87.25-20.25-43.4219-32.9141-56.75-58.25c-13.3359-25.3281-20-55.8281-20-91.5005s6.6641-66.1641,20-91.5c13.3281-25.3281,32.25-44.75,56.75-58.25s53.5781-20.25,87.25-20.25ZM240.5,142.9998c-14.6719,0-27,3.5859-37,10.75-10,7.1719-17.5859,17.9219-22.75,32.25-5.1719,14.3359-7.75,32.3359-7.75,54s2.5781,39.6724,7.75,54.0005c5.1641,14.3359,12.75,25.0859,22.75,32.25,10,7.1719,22.3281,10.75,37,10.75,14.6641,0,26.9141-3.5781,36.75-10.75,9.8281-7.1641,17.25-17.9141,22.25-32.25,5-14.3281,7.5-32.3281,7.5-54.0005s-2.5-39.6641-7.5-54c-5-14.3281-12.4219-25.0781-22.25-32.25-9.8359-7.1641-22.0859-10.75-36.75-10.75Z"/><path class="cls-1" d="M272.1807,464.0884c-12.1387-.001-22.0137-9.877-22.0137-22.0146v-175.3721c0-12.1387,9.876-22.0146,22.0156-22.0146,3.8271,0,7.623,1.0225,10.9775,2.958l151.9902,87.6865c6.8906,3.9746,11.0059,11.0986,11.0059,19.0547.001,7.957-4.1143,15.0811-11.0059,19.0576l-151.9893,87.6855c-3.3545,1.9355-7.1514,2.959-10.9785,2.959h-.002Z"/><path class="cls-2" d="M272.1824,256.6868c1.6638,0,3.3716.4239,4.9821,1.3531l151.9889,87.6859c6.6703,3.8483,6.6703,13.4754,0,17.3237l-151.9889,87.6859c-1.6109.9294-3.3179,1.3531-4.9821,1.3531-5.223,0-10.0152-4.1781-10.0152-10.0149v-175.3718c0-5.8373,4.7917-10.0149,10.0152-10.0149M272.1824,232.6867c-18.756,0-34.0152,15.259-34.0152,34.0149v175.3718c0,18.7559,15.2592,34.0149,34.0152,34.0149,5.9296,0,11.7997-1.5785,16.9758-4.5648l151.9885-87.6857c10.4918-6.0529,17.0094-17.3376,17.0094-29.4503s-6.5177-23.3973-17.0094-29.4503l-151.9889-87.6858c-5.1758-2.9862-11.0459-4.5647-16.9754-4.5647h0Z"/></svg> -------------------------------------------------------------------------------- /docs/src/assets/Splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/Splash.png -------------------------------------------------------------------------------- /docs/src/assets/active-cue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/active-cue.png -------------------------------------------------------------------------------- /docs/src/assets/base-cue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/base-cue.png -------------------------------------------------------------------------------- /docs/src/assets/context-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/context-menu.png -------------------------------------------------------------------------------- /docs/src/assets/cue-playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/cue-playing.png -------------------------------------------------------------------------------- /docs/src/assets/double-click-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/double-click-edit.png -------------------------------------------------------------------------------- /docs/src/assets/drag-drop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/drag-drop.png -------------------------------------------------------------------------------- /docs/src/assets/dummy-cue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/dummy-cue.png -------------------------------------------------------------------------------- /docs/src/assets/hints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/hints.png -------------------------------------------------------------------------------- /docs/src/assets/project-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/project-setup.png -------------------------------------------------------------------------------- /docs/src/assets/qplayer-main-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/qplayer-main-window.png -------------------------------------------------------------------------------- /docs/src/assets/sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/sidebar.png -------------------------------------------------------------------------------- /docs/src/assets/sound-cue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/sound-cue.png -------------------------------------------------------------------------------- /docs/src/assets/stop-cue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/stop-cue.png -------------------------------------------------------------------------------- /docs/src/assets/timecode-cue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/timecode-cue.png -------------------------------------------------------------------------------- /docs/src/assets/volume-cue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/volume-cue.png -------------------------------------------------------------------------------- /docs/src/assets/waveform-viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space928/QPlayer/0d2410b6ec73724085b95d2183c13d28e08ef7bf/docs/src/assets/waveform-viewer.png -------------------------------------------------------------------------------- /docs/src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsLoader } from '@astrojs/starlight/loaders'; 3 | import { docsSchema } from '@astrojs/starlight/schema'; 4 | import { autoSidebarLoader } from 'starlight-auto-sidebar/loader' 5 | import { autoSidebarSchema } from 'starlight-auto-sidebar/schema' 6 | 7 | export const collections = { 8 | docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 9 | autoSidebar: defineCollection({ 10 | loader: autoSidebarLoader(), 11 | schema: autoSidebarSchema(), 12 | }), 13 | }; 14 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | description: A guide on how to get up and running with QPlayer. 4 | --- 5 | 6 | import { Aside } from '@astrojs/starlight/components'; 7 | 8 | <Aside> 9 | This guide is still under construction. For now, have a look through the [reference manual](../../reference/). 10 | </Aside> 11 | -------------------------------------------------------------------------------- /docs/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: QPlayer 3 | description: A powerful media player for theatre. 4 | template: splash 5 | hero: 6 | tagline: A powerful media player for theatre. 7 | image: 8 | file: ../../assets/Logo.svg 9 | actions: 10 | - text: Download Now 11 | link: https://github.com/space928/QPlayer/releases/latest 12 | icon: external 13 | - text: Getting Started 14 | link: guides/getting-started/ 15 | icon: right-arrow 16 | variant: secondary 17 | --- 18 | 19 | import { Card, CardGrid } from '@astrojs/starlight/components'; 20 | 21 | ## Features 22 | 23 | <CardGrid stagger> 24 | <Card title="Free and open source" icon="heart"> 25 | QPlayer is free now and forever, thanks to it's permissive MIT license, 26 | you are free to use and adapt QPlayer however you like! 27 | </Card> 28 | <Card title="Many formats supported" icon="puzzle"> 29 | Supports playback of many common file formats, including: WAV, MP3, etc... 30 | </Card> 31 | <Card title="Intuitive cue stack" icon="list-format"> 32 | Cues containing media files, stop cues, fade cues, and more, are all added to one cue stack 33 | controlled by a single GO button. Multiple cues can be fired at once, cues can be paused, 34 | preloaded, and more. 35 | </Card> 36 | <Card title="Fades & delays" icon="setting"> 37 | Cues can have fade in and out times, start delays, and more to precisly craft your sound scape. 38 | </Card> 39 | <Card title="OSC integration" icon="external"> 40 | Control QPlayer from your favourite software, with simple to integrate OSC messages. 41 | </Card> 42 | <Card title="Show tested" icon="approve-check"> 43 | Reliability is extremely important to a good show! QPlayer, has been used and abused in 44 | a real theatre setting to ensure it's reliability. 45 | </Card> 46 | <Card title="And much more" icon="sun"> 47 | QPlayer is in active development, with many exciting features on the horizon. 48 | </Card> 49 | </CardGrid> 50 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/cue-stack.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: The Cue Stack 3 | order: 10 4 | --- 5 | 6 | import { Kbd } from 'starlight-kbd/components' 7 | 8 | The heart of QPlayer is the cue stack, this is where all media cues, and any other 9 | action that can be triggered by the GO button live. This is an ordered list of all 10 | the cues in the project. For each cue, some common properties are displayed, most 11 | of these are described in more depth on the [Common Cue Properties](../cues/cue) 12 | page. 13 | 14 | ![Cue stack](../../../assets/qplayer-main-window.png) 15 | 16 | ## Playing Cues 17 | 18 | When you press the GO button (<Kbd mac="Space" windows="Space"/>) the currently 19 | selected cue, the one highlighted in the cue stack, is played. When a cue is fired 20 | the next cue in the cue stack is selected. To all active cues in an emergency, 21 | the Stop button can be pressed (<Kbd mac="Esc" windows="Esc"/>). All active cues 22 | are displayed in a list in the left hand panel, from there they can individually be 23 | stopped, paused, and resumed. To pause all active cues, the keyboard shortcut 24 | <Kbd mac="[" windows="["/> can be used pressing the <Kbd mac="]" windows="]"/> 25 | resumes all active cues. 26 | 27 | Cues can be selected in the cue stack by left clicking on them, or you can select 28 | the previous cue by pressing <Kbd mac="Up" windows="Up"/> on the keyboard, or 29 | <Kbd mac="Down" windows="Down"/> to select the next cue in the stack. 30 | 31 | The `Playback` column in the cue stack, displays the duration current playback 32 | progress of the cue. A cue can be "preloaded" to a specified playback time by 33 | selecting the cue, entering a `Preload Time` in the preload panel in the 34 | bottom-left of the main window, and pressing `Preload Cue`. This puts the cue 35 | into a "paused" state at the playback time specified by `Preload Time`. 36 | 37 | ![Active cue](../../../assets/active-cue.png) 38 | 39 | 40 | ## Creating and Editing Cues 41 | 42 | There a few ways to create a cue in QPlayer. The simplest is to right click in 43 | the cue stack on the cue before where you want to insert a cue, this brings up 44 | the cue context menu: 45 | 46 | ![Context menu](../../../assets/context-menu.png) 47 | 48 | From here you can press "Add [Cue Type] Cue" to add the desired cue to the stack. 49 | Cues can also be created by going to the `Edit/Create Cue/Add [Cue Type] Cue` menu, 50 | or by pressing <Kbd mac="Cmd+T" windows="Ctrl+T"/> which also brings up a cue 51 | creation menu. 52 | 53 | Created cues can be duplicated by pressing <Kbd mac="Cmd+D" windows="Ctrl+D"/>, 54 | or by using the button in the right-click menu. They can also be deleted by 55 | pressing <Kbd mac="Delete" windows="Delete"/>, or by using the right click menu. 56 | 57 | Cues can be reordered in the cue stack, by dragging and dropping them, or by using 58 | the keyboard shortcuts <Kbd mac="Cmd+Up" windows="Ctrl+Up"/> and 59 | <Kbd mac="Cmd+Down" windows="Ctrl+Down"/>. Note that reordering cues in the cue stack 60 | will result in them being renumbered, this can have an effect on cues which reference 61 | other cues by Cue ID. See the [Cue ID](../cues/cue#cue-id) page for more information 62 | on Cue IDs. 63 | 64 | 65 | {/* 66 | ![Context menu](../../../assets/context-menu.png) 67 | ![Double click to edit](../../../assets/double-click-edit.png) 68 | ![Drag and drop](../../../assets/drag-drop.png) 69 | ![Sidebar](../../../assets/sidebar.png) 70 | */} 71 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/cues/_meta.yml: -------------------------------------------------------------------------------- 1 | # Fix the sidebar label for the autogenerated group 2 | label: Cues 3 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/cues/cue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Common Cue Properties 3 | order: 0 4 | --- 5 | 6 | All cues in QPlayer share a few common properties which are described here. These 7 | properties can be edited in the `Selected Cue` tab, or for some properties directly 8 | from the cue stack. Additional properties, specific to each cue type are listed in 9 | the `Selected Cue` tab directly after the common properties described here, these 10 | properties are described in more detail in the subsequant pages of this manual. 11 | 12 | ![Cue editor](../../../../assets/base-cue.png) 13 | 14 | ## Cue Information 15 | 16 | ### Cue ID 17 | 18 | This is the decimal number which identifies this cue. Other cues and OSC messages 19 | might reference this cue by it's Cue ID, when changing the Cue ID be sure to check 20 | that other cues referencing it have been updated correctly. 21 | 22 | Cue IDs should be unique within a cue stack and should be in ascending order in the 23 | cue stack. 24 | 25 | :::caution 26 | Currently, QPlayer doesn't stricly enforce order or uniqueness of Cue IDs to 27 | make editing easier, but having duplicate or out of order cues, can result in 28 | unexpected behaviour. 29 | ::: 30 | 31 | ### Cue Name 32 | 33 | A short name for this cue to make it easy to identify in the cue stack. 34 | 35 | ### Description 36 | 37 | A longer description of this cue, or any other notes you might want to leave in this 38 | cue. 39 | 40 | ### Colour 41 | 42 | An accent colour to give the cue in the cue stack to make it easier to identify. 43 | 44 | :::note 45 | This feature is currently unimplemented. 46 | ::: 47 | 48 | ### Enabled 49 | 50 | Whether this cue is enabled or not. Pressing GO on a disabled cue has no effect. 51 | If a cue is disabled it is automatically skipped when advancing the selected cue 52 | after pressing GO. 53 | 54 | ## Timing 55 | 56 | ### Halt 57 | 58 | When disabled, this cue is triggered with the previous cue when it is started. 59 | When the GO button is pressed, the selected cue is triggered as well as all 60 | subsequant cues until a cue with Halt enabled are triggered. 61 | 62 | This can be used to group multiple cues together to start at once or 63 | automatically, one after the other. 64 | 65 | ## Wait 66 | 67 | When the cue is triggered, it waits for the specified amount of time before 68 | actually starting playback. This can be used with `Halt` disabled to 69 | autmatically chain multiple cues together. 70 | 71 | ## Duration 72 | 73 | (Read-only) Shows the computed duration of this cue. To set the duration of this 74 | cue, edit the relevant property specific to the type of cue being edited. 75 | 76 | ## Loop 77 | 78 | The looping behaviour of this cue, can be one of the following: 79 | **`OneShot`** -- This cue is played once when triggered. 80 | **`Looped`** -- This cue plays in a loop for a number of loops specified 81 | by the `Loop Count` property. 82 | **`LoopedInfinite`** -- This cue plays in a loop forever, until it is stopped 83 | manually. 84 | 85 | ## Loop Count 86 | 87 | The number of times this cue should be looped. Requires the `Loop` mode to be set 88 | to `Looped`. 89 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/cues/dummy-cue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dummy Cue 3 | --- 4 | 5 | This cue does nothing. It can be used as an organisational tool in the cue stack or as a 6 | placeholder for other cues. 7 | 8 | ![Dummy cue editor](../../../../assets/dummy-cue.png) 9 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/cues/sound-cue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sound Cue 3 | --- 4 | 5 | ![Sound cue editor](../../../../assets/sound-cue.png) 6 | ![Waveform viewer](../../../../assets/waveform-viewer.png) 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/cues/stop-cue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Stop Cue 3 | --- 4 | 5 | ![Stop cue editor](../../../../assets/stop-cue.png) -------------------------------------------------------------------------------- /docs/src/content/docs/reference/cues/timecode-cue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Timecode Cue 3 | --- 4 | 5 | ![Timecode cue editor](../../../../assets/timecode-cue.png) 6 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/cues/video-cue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Video Cue 3 | --- 4 | 5 | <!--![Video cue editor](../../../../assets/video-cue.png)--> 6 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/cues/volume-cue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Volume Cue 3 | --- 4 | 5 | ![Volume cue editor](../../../../assets/volume-cue.png) 6 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: QPlayer Reference Manual 3 | description: The reference manual for QPlayer. 4 | order: 0 5 | --- 6 | 7 | import { LinkCard } from '@astrojs/starlight/components'; 8 | 9 | ![Splash](../../../assets/Splash.png) 10 | 11 | Welcome to the QPlayer Reference Manual. Here you'll find documentation on all the different features of QPlayer. 12 | 13 | If you're not sure where to start have a look at the Cue Stack page: 14 | <LinkCard title="The Cue Stack" href="cue-stack" /> 15 | 16 | :::note 17 | This manual is still a work in progress, and many parts of it are incomplete. 18 | 19 | Think you can help? Contributions are always welcome on [GitHub](https://github.com/space928/QPlayer). 20 | ::: 21 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/osc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: OSC Reference 3 | order: 30 4 | --- 5 | 6 | QPlayer can be controlled from external applications over the network using 7 | [OSC messages](http://opensoundcontrol.org/). At the moment only UDP OSC messages are 8 | supported. Configuration settings for sending and receiving OSC messages can be found 9 | in the [Project Setup](../project-setup). 10 | 11 | QPlayer can also remote control other instances of itself or compatible applications 12 | over OSC. This can be useful if you need to control multiple media players from a 13 | single computer. See the [OSC Remote](#osc-remote) section for more details. 14 | 15 | ## Received Messages 16 | 17 | The following OSC messages can be received by QPlayer: 18 | 19 | #### Anatomy of an OSC message: 20 | 21 | ``` 22 | /qplayer/test,(param1),[param2] 23 | ↓ | | 24 | OSC Address ↓ | 25 | First argument 26 | ↓ 27 | Second argument (optional) 28 | ``` 29 | 30 | An OSC message is comprised of an *address*, which may be made up of multiple parts 31 | separated by `/` characters. This is followed by any number of *arguments* which 32 | may be an integer, a decimal, a string, or one of the other types specified 33 | [here](https://opensoundcontrol.stanford.edu/spec-1_0.html#osc-type-tag-string). 34 | (For developpers) Note that this description is a slight simplification of the 35 | format, please read the [specification](https://opensoundcontrol.stanford.edu/spec-1_0.html) 36 | for more information on implementing OSC yourself. 37 | 38 | ### Go 39 | ``` 40 | /qplayer/go,[qid],[select] 41 | ``` 42 | 43 | Triggers the specified cue, or the currently selected cue if no cue is specified. 44 | Optionally, also selects the next cue in the cue stack after the specified cue is 45 | fired. 46 | 47 | #### Arguments: 48 | `[qid]` *(optional, string or float)* the cue ID to trigger. 49 | `[select]` *(optional, any type)* when this argument is present, the specified cue 50 | is selected before pressing GO. 51 | 52 | ### Pause 53 | ``` 54 | /qplayer/pause,[qid] 55 | ``` 56 | 57 | Pauses the playback of the specified cue, or the currently selected cue if no cue 58 | is specified. Has no effect if the cue is not currently active. 59 | 60 | #### Arguments: 61 | `[qid]` *(optional, string or float)* the cue ID to pause. 62 | 63 | ### Unpause 64 | ``` 65 | /qplayer/unpause,[qid] 66 | ``` 67 | 68 | Unpauses the specified cue, or the currently selected cue if no cue is specified. 69 | Has no effect if the cue is not currently paused or preloaded. 70 | 71 | #### Arguments: 72 | `[qid]` *(optional, string or float)* the cue ID to unpause. 73 | 74 | ### Stop 75 | ``` 76 | /qplayer/stop,[qid] 77 | ``` 78 | 79 | Stops the playback of the specified cue, or all active cues if no cue is specified. 80 | 81 | #### Arguments: 82 | `[qid]` *(optional, string or float)* the cue ID to stop. 83 | 84 | ### Preload 85 | ``` 86 | /qplayer/preload,[qid],[time] 87 | ``` 88 | 89 | Preloads the specified cue to the given time, or the currently selected cue if no 90 | cue is specified. If no time is specified, defaults to the preload time set in the 91 | UI. 92 | 93 | #### Arguments: 94 | `[qid]` *(optional, string or float)* the cue ID to preload. 95 | `[time]` *(optional, string or float)* the time in seconds to preload the cue to. 96 | 97 | ### Select Cue 98 | ``` 99 | /qplayer/select,[qid] 100 | ``` 101 | 102 | Selects the specified cue in the cue stack by its cue ID. 103 | 104 | #### Arguments: 105 | `[qid]` *(string or float)* the cue ID to select. 106 | 107 | ### Select Previous Cue 108 | ``` 109 | /qplayer/up 110 | ``` 111 | 112 | Selects the previous cue in the cue stack. 113 | 114 | ### Select Next Cue 115 | ``` 116 | /qplayer/down 117 | ``` 118 | 119 | Selects the next cue in the cue stack. 120 | 121 | ### Save Project File 122 | ``` 123 | /qplayer/save 124 | ``` 125 | 126 | Saves current QPlayer project under the same name. 127 | 128 | ## Sent Messages 129 | 130 | *None so far.* 131 | 132 | # OSC Remote 133 | 134 | :::note 135 | This section is mostly of relevance to advanced users wishing to understand how 136 | remote cues work. For information on setting up remote control, see the relevant 137 | sections on the [Project Setup](../project-setup) and 138 | [Common Cue Properties](../cues/cue) pages. 139 | ::: 140 | 141 | A given cue in a cue stack can be designated as a remote cue by setting its 142 | "Remote Target" property (see: [Common Cue Properties](../cues/cue) for more 143 | details). When a cue is designated as a remote cue, triggering it results in 144 | an OSC message being sent to the specified remote client. The remote client is 145 | in turn expected to reply with playback status messages. Remote clients can be 146 | viewed and configured from the [Project Setup](../project-setup) tab. 147 | 148 | ## Messages Sent To Remote Clients 149 | 150 | ### Go 151 | ``` 152 | /qplayer/remote/go,(target),(qid) 153 | ``` 154 | 155 | #### Arguments: 156 | `(target)` *(string)* the name of the remote client. 157 | `(qid)` *(string or float)* the cue ID to trigger. 158 | 159 | ### Pause 160 | ``` 161 | /qplayer/remote/pause,(target),(qid) 162 | ``` 163 | 164 | #### Arguments: 165 | `(target)` *(string)* the name of the remote client. 166 | `(qid)` *(string or float)* the cue ID to trigger. 167 | 168 | ### Unpause 169 | ``` 170 | /qplayer/remote/unpause,(target),(qid) 171 | ``` 172 | 173 | #### Arguments: 174 | `(target)` *(string)* the name of the remote client. 175 | `(qid)` *(string or float)* the cue ID to trigger. 176 | 177 | ### Stop 178 | ``` 179 | /qplayer/remote/stop,(target),(qid) 180 | ``` 181 | 182 | #### Arguments: 183 | `(target)` *(string)* the name of the remote client. 184 | `(qid)` *(string or float)* the cue ID to trigger. 185 | 186 | ### Preload 187 | ``` 188 | /qplayer/remote/preload,(target),(qid),(time) 189 | ``` 190 | 191 | #### Arguments: 192 | `(target)` *(string)* the name of the remote client. 193 | `(qid)` *(string or float)* the cue ID to trigger. 194 | `(time)` *(string or float)* the time in seconds to preload to. 195 | 196 | ### Update Showfile 197 | ``` 198 | /qplayer/remote/update-show,(target),(showfile) 199 | ``` 200 | 201 | This command can be used to resynchronise the show file between the host and 202 | the client. This action may cause an interruption in playback on the client. 203 | 204 | #### Arguments: 205 | `(target)` *(string)* the name of the remote client. 206 | `(showfile)` *(blob)* the contents of the showfile as a utf-8 encoded json file. 207 | 208 | ### Ping 209 | ``` 210 | /qplayer/remote/ping,(target) 211 | ``` 212 | 213 | Sends a ping to a remote client, which should reply with a `pong` message. 214 | 215 | #### Arguments: 216 | `(target)` *(string)* the name of the remote client. 217 | 218 | ## Messages Sent From Remote Clients 219 | 220 | ### Discovery 221 | ``` 222 | /qplayer/remote/discovery,(name) 223 | ``` 224 | 225 | This message should be sent roughly once per second by remote clients to allow them 226 | to be discovered by the host. The host should maintain a list of clients which have 227 | sent a discovery message in the last 5 seconds. 228 | 229 | #### Arguments: 230 | `(name)` *(string)* the name of the remote client. 231 | 232 | 233 | ### Pong 234 | ``` 235 | /qplayer/remote/pong,(name) 236 | ``` 237 | 238 | A reply to a `ping` message. 239 | 240 | #### Arguments: 241 | `(name)` *(string)* the name of the remote client. 242 | 243 | ### Cue Status 244 | ``` 245 | /qplayer/remote/fb/cue-status,(name),(qid),(state),[time] 246 | ``` 247 | 248 | The remote client is expected to send a cue status message any time the state of a 249 | cue changes or when the playback time of the cue changes (at a maximum rate of 10 250 | times per second). 251 | 252 | ```cs 253 | public enum CueState 254 | { 255 | // The Cue is currently stopped and ready to be played 256 | Ready = 0, 257 | // The Cue is currently waiting to start. 258 | Delay = 1, 259 | // The Cue is currently active. 260 | Playing = 2, 261 | // The Cue is currently playing in a looped mode. 262 | PlayingLooped = 3, 263 | // This state applies to cues which have been paused or preloaded. 264 | Paused = 4, 265 | } 266 | ``` 267 | 268 | #### Arguments: 269 | `(name)` *(string)* the name of the remote client. 270 | `(qid)` *(string or float)* the cue ID being reported. 271 | `(state)` *(int)* the current state of the cue as defined in the `CueState` 272 | enumeration. 273 | `[time]` *(optional, float)* the current playback time in seconds of the cue. 274 | 275 | <!--![Cue Stack](../../../assets/hints.png)--> -------------------------------------------------------------------------------- /docs/src/content/docs/reference/project-setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Project Setup 3 | order: 20 4 | --- 5 | 6 | From this panel, global settings related to the QPlayer project can be 7 | configured. All settings in this panel are saved to the project file. 8 | 9 | ![Cue Stack](../../../assets/project-setup.png) 10 | 11 | ## Project Metadata 12 | 13 | The settings in this section, are simply metadata for the show file, they have 14 | no use outside of this section. 15 | 16 | ### Project Name 17 | 18 | The name of the QPlayer project. 19 | 20 | ### Description 21 | 22 | A longer description of the project, or a place to keep some notes. 23 | 24 | ### Author 25 | 26 | The author of the QPlayer project. 27 | 28 | ### Show Date 29 | 30 | A field to store the show date, or project creation date. 31 | 32 | ## Audio Setup 33 | 34 | In this section, the audio output device(s) can be configured. 35 | 36 | ### Audio Latency 37 | 38 | The desired latency in milliseconds of the audio driver. Note that the total 39 | latency will be slightly higher than what is specified here due to other 40 | processing overheads. Some audio drivers don't support setting a latency, on 41 | these drivers, this setting has no effect. 42 | 43 | Recommended latency settings: 44 | `Wave` -- 100 ms 45 | `DirectSound` -- 60 ms 46 | `WASAPI` -- 10 ms 47 | `ASIO` -- *setting has no effect* (configure the latency from the ASIO 48 | driver) 49 | 50 | ### Sound Driver 51 | 52 | Which audio driver should be used to output audio. Different audio hardware 53 | will compatible with different drivers (notably the case for ASIO hardware), 54 | and different operating systems will have different drivers available. 55 | 56 | On Windows, the `WASAPI` driver or, if your audio hardware supports it, the 57 | `ASIO` driver is recommended. 58 | 59 | ### Output Device 60 | Which audio device to output to. 61 | 62 | When saving the project, QPlayer saves the name of the selected output device 63 | to the project; when attempting to load the project file, it tries to match 64 | the name in the project file to the names of the available devices, if the 65 | available audio devices change between saving and reloading the project (or if 66 | the project is loaded on a different computer), QPlayer may fail to find the 67 | selected device. 68 | 69 | ## Network Setup 70 | 71 | ### Network Adapter 72 | 73 | Which network adapter to use for communication (at the moment this is only 74 | used for OSC). 75 | 76 | When loading a project from the disk, check this setting is correct in case 77 | your machine's IP has changed since the project was saved. 78 | 79 | Note that when attempting to communicate between two OSC devices on the same 80 | computer, each device may need to be on a separate network adapter so that 81 | they can see each other's messages. This is a limitation of the operating 82 | system. 83 | 84 | ### OSC TX Port 85 | 86 | The port to send OSC messages to. This should correspond with the RX port 87 | of the device receiving OSC; the exception to this rule is with 88 | [remote nodes](#remote-nodes), which should have matching TX and RX ports. 89 | 90 | ### OSC RX Port 91 | 92 | The port to receive OSC messages from. This should correspond with the TX 93 | port of the device sending OSC messages to QPlayer. 94 | 95 | ### Monitor OSC Messages 96 | 97 | When enabled, reports all received and transmitted OSC messages to the log 98 | window (Window/Log Window). This can be useful in debugging whether or not 99 | QPlayer is receiving OSC messages correctly. 100 | 101 | ## Remote Nodes 102 | 103 | QPlayer can control instances of itself running on another computer, or 104 | other software compatible with the protocol defined in 105 | [OSC Remote](../osc#osc-remote). Remote nodes should each have a unique 106 | name used to identify them. These nodes will send discovery messages every 107 | second to the network allowing the host QPlayer instance to discover them. 108 | If a remote client isn't heard from in within 5 seconds, the host instance of 109 | QPlayer will show a warning that a node can't be reached. If you can't see a 110 | node in the (discovery) panel, check the OSC TX and RX ports match and check 111 | your network setup (try pinging the remote node from a command line). 112 | 113 | :::note 114 | This section is still under construction. 115 | ::: 116 | -------------------------------------------------------------------------------- /docs/src/styles/custom.css: -------------------------------------------------------------------------------- 1 | /* https://starlight.astro.build/guides/css-and-tailwind/#color-theme-editor */ 2 | /* Dark mode colors. */ 3 | :root { 4 | --sl-color-accent-low: #0b2c0b; 5 | --sl-color-accent: #00810f; 6 | --sl-color-accent-high: #afd5ad; 7 | --sl-color-white: #ffffff; 8 | --sl-color-gray-1: #e4f0f2; 9 | --sl-color-gray-2: #b6c5c7; 10 | --sl-color-gray-3: #759296; 11 | --sl-color-gray-4: #425e61; 12 | --sl-color-gray-5: #233d41; 13 | --sl-color-gray-6: #112b2f; 14 | --sl-color-black: #0f1a1c; 15 | } 16 | /* Light mode colors. */ 17 | :root[data-theme='light'] { 18 | --sl-color-accent-low: #c4e0c2; 19 | --sl-color-accent: #007c0e; 20 | --sl-color-accent-high: #043e06; 21 | --sl-color-white: #0f1a1c; 22 | --sl-color-gray-1: #112b2f; 23 | --sl-color-gray-2: #233d41; 24 | --sl-color-gray-3: #425e61; 25 | --sl-color-gray-4: #759296; 26 | --sl-color-gray-5: #b6c5c7; 27 | --sl-color-gray-6: #e4f0f2; 28 | --sl-color-gray-7: #f1f8f8; 29 | --sl-color-black: #ffffff; 30 | } 31 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [".astro/types.d.ts", "**/*"], 4 | "exclude": ["dist"] 5 | } 6 | --------------------------------------------------------------------------------