├── .dockerignore ├── .editorconfig ├── .github ├── actions │ ├── branch-exists │ │ └── action.yml │ ├── cache │ │ └── action.yml │ ├── check-secret │ │ └── action.yml │ ├── extract-version │ │ └── action.yml │ ├── test │ │ └── action.yml │ └── validate-dependencies │ │ └── action.yml ├── dependabot.yml ├── pull_request_template.md ├── version └── workflows │ ├── node-ci.dev.yml │ └── node-ci.prod.yml ├── .gitignore ├── .jshintrc ├── .npmignore ├── .prettierrc ├── .vscode ├── launch.json └── tasks.json ├── CHANGELOG.md ├── Dockerfile ├── Docs ├── CasparCG-Example │ ├── casparcg.config │ └── sisyfos-casparcg-geometry.json └── pix │ ├── AdvancedChannelStrip.png │ ├── SisyfosChannelStripdescription.jpg │ ├── SisyfosChanneldescription.jpg │ ├── mic-tally-view.png │ ├── minimonitorview.png │ └── sisyfos.png ├── LICENSE ├── README.md ├── client ├── @types │ └── react-slider │ │ └── index.d.ts ├── favicon │ └── favicon.ico ├── index.ejs ├── index.tsx ├── package.json ├── src │ ├── assets │ │ └── css │ │ │ ├── App.css │ │ │ ├── CcgChannelSettings.css │ │ │ ├── ChanStrip.css │ │ │ ├── ChanStripEq.css │ │ │ ├── ChanStripFull.css │ │ │ ├── Channel.css │ │ │ ├── ChannelMonitorOptions.css │ │ │ ├── ChannelRouteSettings.css │ │ │ ├── Channels.css │ │ │ ├── LabelSettings.css │ │ │ ├── MicTally.css │ │ │ ├── MiniChanStrip.css │ │ │ ├── MiniChannel.css │ │ │ ├── MiniChannels.css │ │ │ ├── NoUiSlider.css │ │ │ ├── PagesSettings.css │ │ │ ├── ReductionMeter.css │ │ │ ├── RoutingStorage.css │ │ │ ├── Settings.css │ │ │ └── VuMeter.css │ ├── components │ │ ├── App.tsx │ │ ├── ChanStrip.tsx │ │ ├── ChanStripEq.tsx │ │ ├── ChanStripFull.tsx │ │ ├── Channel.tsx │ │ ├── ChannelMonitorOptions.tsx │ │ ├── ChannelRouteSettings.tsx │ │ ├── Channels.tsx │ │ ├── Labels.tsx │ │ ├── MicTally.tsx │ │ ├── MiniChanStrip.tsx │ │ ├── MiniChannel.tsx │ │ ├── MiniChannels.tsx │ │ ├── PagesSettings.tsx │ │ ├── ReductionMeter.tsx │ │ ├── RoutingStorage.tsx │ │ ├── Settings.tsx │ │ └── VuMeter.tsx │ └── utils │ │ ├── SocketClientHandlers.ts │ │ ├── i18n.ts │ │ └── labels.ts ├── tsconfig.json └── webpack.config.js ├── desktop ├── electron.js ├── package.json └── prepare-package.js ├── package.json ├── server ├── @types │ ├── classnames │ │ └── index.d.ts │ ├── emberplus │ │ └── index.d.ts │ ├── osc │ │ └── index.d.ts │ └── web-midi-api │ │ └── index.d.ts ├── __tests__ │ ├── __mocks__ │ │ ├── parsedEmptyStore.json │ │ ├── parsedFullStore.json │ │ └── parsedSimpleStore.json │ ├── channelReducer.spec.ts │ ├── faderReducer.spec.ts │ ├── indexReducer.spec.ts │ └── settingsReducer.spec.ts ├── index.ts ├── jest.config.js ├── package.json ├── src │ ├── MainThreadHandler.ts │ ├── expressHandler.ts │ ├── mainClasses.ts │ ├── reducers │ │ └── store.ts │ └── utils │ │ ├── AutomationConnection.ts │ │ ├── MixerConnection.ts │ │ ├── RemoteConnection.ts │ │ ├── SettingsStorage.ts │ │ ├── SnapshotHandler.ts │ │ ├── labels.ts │ │ ├── logger.ts │ │ ├── migrations.ts │ │ ├── mixerConnections │ │ ├── AtemConnection.ts │ │ ├── CasparCGConnection.ts │ │ ├── EmberMixerConnection.ts │ │ ├── LawoRubyConnection.ts │ │ ├── MidiMixerConnection.ts │ │ ├── OscMixerConnection.ts │ │ ├── SSLMixerConnection.ts │ │ ├── StuderMixerConnection.ts │ │ ├── StuderVistaMixerConnection.ts │ │ ├── VMixMixerConnection.ts │ │ ├── YamahaQlClConnection.ts │ │ └── productSpecific │ │ │ ├── behringerXr.ts │ │ │ └── midas.ts │ │ ├── remoteConnections │ │ ├── HuiMidiRemoteConnection.ts │ │ └── SkaarhojRemoteConnection.ts │ │ └── vuServer.ts └── tsconfig.json ├── shared ├── package.json └── src │ ├── actions │ ├── channelActions.ts │ ├── faderActions.ts │ ├── settingsActions.ts │ └── utils │ │ └── dbConversion.ts │ ├── constants │ ├── AutomationPresets.ts │ ├── MixerProtocolInterface.ts │ ├── MixerProtocolPresets.ts │ ├── SOCKET_IO_DISPATCHERS.ts │ ├── mixerProtocols │ │ ├── DmxIs.ts │ │ ├── LawoMC2.ts │ │ ├── LawoRelayVrx4.ts │ │ ├── LawoRuby.ts │ │ ├── SSLsystemT.ts │ │ ├── StuderOnAirEmber.ts │ │ ├── StuderVistaEmber.ts │ │ ├── ardourMaster.ts │ │ ├── atem.ts │ │ ├── behringerXrMaster.ts │ │ ├── casparCGMaster.ts │ │ ├── genericMidi.ts │ │ ├── midasMaster.ts │ │ ├── reaperMaster.ts │ │ ├── vMix.ts │ │ └── yamahaQLCL.ts │ └── remoteProtocols │ │ ├── HuiRemoteFaderPresets.ts │ │ └── SkaarhojProtocol.ts │ ├── reducers │ ├── channelsReducer.ts │ ├── fadersReducer.ts │ ├── indexReducer.ts │ ├── settingsReducer.ts │ └── store.ts │ └── utils │ └── vu-server-types.ts ├── storage ├── Default.x32 ├── Preset 1.x32 ├── Preset 2.x32 ├── Preset 3.x32 ├── default-casparcg.ccg └── pages.json ├── tslint.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.vscode 3 | *.github 4 | Dockerfile 5 | Docs 6 | 7 | */dist 8 | desktop 9 | 10 | .gitignore 11 | .dockerignore 12 | .prettierignore 13 | .prettierrc.json 14 | jest.config.js 15 | tsconfig.jest.json 16 | tslint.json 17 | .npmignore 18 | .jshintrc 19 | .editorconfig 20 | webpack.config.js 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = crlf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.github/actions/branch-exists/action.yml: -------------------------------------------------------------------------------- 1 | ####################################################################################################################### 2 | # NOTE: DOES NOT WORK YET 3 | ####################################################################################################################### 4 | 5 | name: 'Branch exists' 6 | description: 'Check that branch exists' 7 | inputs: 8 | branch: 9 | description: 'branch to check existence for' 10 | required: true 11 | outputs: 12 | exists: 13 | description: "Boolean of whether branch exists" 14 | value: ${{ steps.branch.outputs.exists }} 15 | runs: 16 | using: 'composite' 17 | steps: 18 | - name: Check branch 19 | id: branch 20 | env: 21 | branch: ${{ inputs.branch }} 22 | shell: bash 23 | run: | 24 | if [ -z $(git show-ref -- heads/$branch) ]; then 25 | echo ::set-output name=exists::false 26 | else 27 | echo ::set-output name=exists::true 28 | fi 29 | -------------------------------------------------------------------------------- /.github/actions/cache/action.yml: -------------------------------------------------------------------------------- 1 | name: "Cache dependencies" 2 | description: "Cache dependencies" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Cache dependencies 7 | uses: actions/setup-node@v3 8 | with: 9 | node-version: ${{ env.node-version }} 10 | cache: ${{ env.node-package-manager }} -------------------------------------------------------------------------------- /.github/actions/check-secret/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Check secret' 2 | description: 'Check secret' 3 | inputs: 4 | secret: 5 | description: 'Secret to check' 6 | required: true 7 | outputs: 8 | defined: 9 | description: "Boolean of whether secret is defined" 10 | value: ${{ steps.secret.outputs.defined }} 11 | runs: 12 | using: 'composite' 13 | steps: 14 | - name: Check secrets 15 | id: secret 16 | env: 17 | secret: ${{ inputs.secret }} 18 | shell: bash 19 | run: | 20 | if [ "$secret" == "" ]; then 21 | echo ::set-output name=defined::false 22 | else 23 | echo ::set-output name=defined::true 24 | fi 25 | -------------------------------------------------------------------------------- /.github/actions/extract-version/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Extract version' 2 | description: 'Split semver version into major, minor and patch (and version).' 3 | inputs: 4 | version: 5 | description: 'Version as string' 6 | required: true 7 | outputs: 8 | version: 9 | description: "Full version" 10 | value: ${{ steps.match.outputs.group1 }} 11 | major: 12 | description: "Major version" 13 | value: ${{ steps.match.outputs.group2 }} 14 | minor: 15 | description: "Minor version" 16 | value: ${{ steps.match.outputs.group3 }} 17 | patch: 18 | description: "Patch version" 19 | value: ${{ steps.match.outputs.group4 }} 20 | runs: 21 | using: 'composite' 22 | steps: 23 | - run: echo "${{ inputs.version }}" 24 | shell: bash 25 | - name: Match Semantic Version 26 | uses: actions-ecosystem/action-regex-match@v2 27 | id: match 28 | with: 29 | text: ${{ inputs.version }} 30 | regex: '^v?(?(?[0-9]+).(?[0-9]+).(?[0-9]+))$' 31 | -------------------------------------------------------------------------------- /.github/actions/test/action.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | description: "Test" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - uses: ./.github/actions/cache 7 | - name: Install dependencies 8 | run: yarn install --frozen-lockfile 9 | shell: bash 10 | - name: Run tests 11 | run: yarn test 12 | shell: bash -------------------------------------------------------------------------------- /.github/actions/validate-dependencies/action.yml: -------------------------------------------------------------------------------- 1 | name: "Validate dependencies" 2 | description: "Validate dependencies" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Check dependencies 7 | run: yarn audit --groups "dependencies" --level moderate 8 | shell: bash -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | target-branch: 'develop' 5 | directory: '/' 6 | schedule: 7 | interval: 'weekly' 8 | 9 | - package-ecosystem: 'github-actions' 10 | target-branch: 'develop' 11 | directory: '/' 12 | schedule: 13 | interval: 'weekly' 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Important! This repository is not actively maintained 2 | If you are planning to send a PR or open an issue, we suggest sending it to https://github.com/olzzon/sisyfos-audio-controller which is an actively maintained fork of this codebase. We may merge code from that repository at our discretion. -------------------------------------------------------------------------------- /.github/version: -------------------------------------------------------------------------------- 1 | 1.11.6-locally-modified 2 | -------------------------------------------------------------------------------- /.github/workflows/node-ci.dev.yml: -------------------------------------------------------------------------------- 1 | name: Dev Node CI 2 | 3 | env: 4 | node-version: 18 5 | node-package-manager: yarn 6 | 7 | on: 8 | push: 9 | branches: 10 | - "develop" 11 | 12 | jobs: 13 | cache-dependencies: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Access repository 17 | uses: actions/checkout@v4 18 | - uses: ./.github/actions/cache 19 | - name: Install dependencies 20 | run: yarn install --frozen-lockfile 21 | 22 | test: 23 | runs-on: ubuntu-latest 24 | needs: cache-dependencies 25 | steps: 26 | - name: Access repository 27 | uses: actions/checkout@v4 28 | - uses: ./.github/actions/test 29 | 30 | validate-dependencies: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Access repository 34 | uses: actions/checkout@v4 35 | - uses: ./.github/actions/validate-dependencies 36 | 37 | build: 38 | runs-on: ubuntu-latest 39 | needs: 40 | - test 41 | - validate-dependencies 42 | steps: 43 | - name: Access repository 44 | uses: actions/checkout@v2 45 | - uses: ./.github/actions/cache 46 | 47 | - name: Install dependencies 48 | run: yarn install --frozen-lockfile 49 | 50 | - name: Build 51 | run: yarn build 52 | 53 | - name: Upload client build artifact 54 | uses: actions/upload-artifact@v3 55 | with: 56 | name: ${{ github.event.repository.name }}-client-develop 57 | path: client/dist 58 | 59 | - name: Upload server build artifact 60 | uses: actions/upload-artifact@v3 61 | with: 62 | name: ${{ github.event.repository.name }}-server-develop 63 | path: server/dist 64 | 65 | check-docker-credentials: 66 | runs-on: ubuntu-latest 67 | needs: build 68 | outputs: 69 | defined: ${{ steps.username.outputs.defined == 'true' && steps.password.outputs.defined == 'true' }} 70 | steps: 71 | - name: Access repository 72 | uses: actions/checkout@v4 73 | 74 | - name: Check if has username 75 | id: username 76 | uses: ./.github/actions/check-secret 77 | with: 78 | secret: ${{ secrets.DOCKER_USERNAME }} 79 | 80 | - name: Check if has password 81 | id: password 82 | uses: ./.github/actions/check-secret 83 | with: 84 | secret: ${{ secrets.DOCKER_PASSWORD }} 85 | 86 | publish-docker-image: 87 | runs-on: ubuntu-latest 88 | if: needs.check-docker-credentials.outputs.defined == 'true' 89 | needs: 90 | - check-docker-credentials 91 | steps: 92 | - name: Access repository 93 | uses: actions/checkout@v4 94 | 95 | - uses: actions/download-artifact@v3 96 | with: 97 | name: ${{ github.event.repository.name }}-client-develop 98 | path: client/dist 99 | 100 | - uses: actions/download-artifact@v3 101 | with: 102 | name: ${{ github.event.repository.name }}-server-develop 103 | path: server/dist 104 | 105 | - name: Docker meta 106 | id: meta 107 | uses: docker/metadata-action@v4 108 | with: 109 | images: "tv2media/${{ github.event.repository.name }}" 110 | tags: | 111 | type=ref,event=branch 112 | 113 | - name: Log in to Docker Hub 114 | uses: docker/login-action@v2 115 | with: 116 | username: ${{ secrets.DOCKER_USERNAME }} 117 | password: ${{ secrets.DOCKER_PASSWORD }} 118 | 119 | - name: Build and push 120 | uses: docker/build-push-action@v4 121 | with: 122 | context: . 123 | push: true 124 | tags: ${{ steps.meta.outputs.tags }} 125 | labels: ${{ steps.meta.outputs.labels }} 126 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build folder and files # 2 | ########################## 3 | builds/ 4 | 5 | # Development folders and files # 6 | ################################# 7 | .tmp/ 8 | dist/ 9 | coverage/ 10 | node_modules/ 11 | release/ 12 | *.compiled.* 13 | settings.json 14 | pages.json 15 | *.shot 16 | *.ccg 17 | 18 | # Folder config file # 19 | ###################### 20 | Desktop.ini 21 | 22 | # Folder notes # 23 | ################ 24 | _ignore/ 25 | 26 | # Log files & folders # 27 | ####################### 28 | logs/ 29 | *.log 30 | npm-debug.log* 31 | .npm 32 | 33 | # Packages # 34 | ############ 35 | # it's better to unpack these files and commit the raw source 36 | # git has its own built in compression methods 37 | *.7z 38 | *.dmg 39 | *.gz 40 | *.iso 41 | *.jar 42 | *.rar 43 | *.tar 44 | *.zip 45 | 46 | # Photoshop & Illustrator files # 47 | ################################# 48 | *.ai 49 | *.eps 50 | *.psd 51 | 52 | # Windows & Mac file caches # 53 | ############################# 54 | .DS_Store 55 | Thumbs.db 56 | ehthumbs.db 57 | 58 | # Windows shortcuts # 59 | ##################### 60 | *.lnk 61 | 62 | # Exclude JetBrains specific files 63 | .idea 64 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | _ignore/ 2 | docs/ 3 | builds/ 4 | dist/ 5 | .editorconfig 6 | code-of-conduct.md 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "outputCapture": "std", 11 | "name": "Debug Jest Tests", 12 | "cwd": "${workspaceFolder}", 13 | "args": [ 14 | "--inspect-brk", 15 | "${workspaceRoot}/node_modules/.bin/jest", 16 | "--runInBand", 17 | "--config", 18 | "${workspaceRoot}/jest.config.js" 19 | ], 20 | "windows": { 21 | "args": [ 22 | "--inspect-brk", 23 | "${workspaceRoot}/node_modules/jest/bin/jest.js", 24 | "--runInBand", 25 | "--config", 26 | "${workspaceRoot}/jest.config.js" 27 | ], 28 | }, 29 | "console": "integratedTerminal", 30 | "internalConsoleOptions": "neverOpen" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "start", 8 | "type": "npm", 9 | "script": "start", 10 | "problemMatcher": [ 11 | "$tsc" 12 | ], 13 | "isBackground": true, 14 | "promptOnClose": false 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # BUILD IMAGE 2 | FROM node:18.16-alpine 3 | RUN apk add --no-cache git 4 | WORKDIR /opt/sisyfos-audio-controller 5 | COPY . . 6 | RUN yarn --check-files --frozen-lockfile 7 | RUN yarn build 8 | RUN yarn --check-files --frozen-lockfile --production --force 9 | RUN yarn cache clean 10 | 11 | # DEPLOY IMAGE 12 | FROM node:18.16-alpine 13 | WORKDIR /opt/sisyfos-audio-controller 14 | COPY --from=0 /opt/sisyfos-audio-controller . 15 | EXPOSE 1176/tcp 16 | EXPOSE 1176/udp 17 | EXPOSE 5255/tcp 18 | EXPOSE 5255/udp 19 | CMD ["yarn", "start"] 20 | -------------------------------------------------------------------------------- /Docs/CasparCG-Example/casparcg.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | d:/casparcg/media/ 5 | d:/casparcg/log/ 6 | d:/casparcg/data/ 7 | d:/casparcg/template/ 8 | d:/casparcg/thumbnail/ 9 | d:/casparcg/font/ 10 | 11 | debug 12 | 13 | true 14 | false 15 | true 16 | 17 | 18 | 19 | 1080i5000 20 | 8ch2 21 | 22 | 23 | 7 24 | true 25 | 16ch 26 | normal 27 | 5 28 | 29 | 30 | 31 | 32 | 1080i5000 33 | 16ch 34 | 35 | 36 | 8 37 | true 38 | 16ch 39 | normal 40 | 5 41 | 42 | 43 | 44 | 45 | 1080i5000 46 | 16ch 47 | 48 | 49 | 4 50 | true 51 | 16ch 52 | normal 53 | 5 54 | 55 | 56 | 57 | 58 | 59 | false 60 | 61 | 62 | 63 | 5250 64 | AMCP 65 | 66 | 67 | 3250 68 | LOG 69 | 70 | 71 | 72 | 6250 73 | 74 | 75 |
192.168.0.32
76 | 8000 77 |
78 |
79 |
80 | 95 |
96 | -------------------------------------------------------------------------------- /Docs/CasparCG-Example/sisyfos-casparcg-geometry.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Sofie CasparCG Example", 3 | "fromMixer": { 4 | "CHANNEL_VU": [ 5 | ["/channel/1/stage/layer/51/audio/1/pFS", "/channel/1/stage/layer/51/audio/2/pFS"], 6 | ["/channel/1/stage/layer/52/audio/1/pFS", "/channel/1/stage/layer/52/audio/2/pFS"], 7 | ["/channel/1/stage/layer/53/audio/1/pFS", "/channel/1/stage/layer/53/audio/2/pFS"], 8 | ["/channel/1/stage/layer/54/audio/1/pFS", "/channel/1/stage/layer/54/audio/2/pFS"], 9 | ["/channel/1/stage/layer/55/audio/1/pFS", "/channel/1/stage/layer/55/audio/2/pFS"], 10 | ["/channel/1/stage/layer/56/audio/1/pFS", "/channel/1/stage/layer/56/audio/2/pFS"] 11 | ] 12 | }, 13 | "toMixer": { 14 | "PFL_AUX_FADER_LEVEL": [ 15 | [ 16 | { "channel": 2, "layer": 51 } 17 | ], 18 | [ 19 | { "channel": 2, "layer": 52 } 20 | ], 21 | [ 22 | { "channel": 2, "layer": 53 } 23 | ], 24 | [ 25 | { "channel": 2, "layer": 54 } 26 | ], 27 | [ 28 | { "channel": 2, "layer": 55 } 29 | ], 30 | [ 31 | { "channel": 2, "layer": 56 } 32 | ] 33 | ], 34 | "PGM_CHANNEL_FADER_LEVEL": [ 35 | [ 36 | { "channel": 1, "layer": 51 }, 37 | { "channel": 3, "layer": 51 } 38 | ], 39 | [ 40 | { "channel": 1, "layer": 52 }, 41 | { "channel": 3, "layer": 52 } 42 | ], 43 | [ 44 | { "channel": 1, "layer": 53 }, 45 | { "channel": 3, "layer": 53 } 46 | ], 47 | [ 48 | { "channel": 1, "layer": 54 }, 49 | { "channel": 3, "layer": 54 } 50 | ], 51 | [ 52 | { "channel": 1, "layer": 55 }, 53 | { "channel": 3, "layer": 55 } 54 | ], 55 | [ 56 | { "channel": 1, "layer": 56 }, 57 | { "channel": 3, "layer": 56 } 58 | ] 59 | ] 60 | }, 61 | "channelLabels": [ 62 | "RM1", 63 | "RM2", 64 | "RM3", 65 | "RM4", 66 | "RM5", 67 | "MP1" 68 | ], 69 | "sourceOptions": { 70 | "sources": [ 71 | { "channel": 2, "layer": 51 }, 72 | { "channel": 2, "layer": 52 }, 73 | { "channel": 2, "layer": 53 }, 74 | { "channel": 2, "layer": 54 }, 75 | { "channel": 2, "layer": 55 }, 76 | { "channel": 2, "layer": 56 } 77 | ], 78 | "options": { 79 | "CHANNEL_LAYOUT": { 80 | "1L-2R": "8ch2", 81 | "1L-1R": "4ch-dleft", 82 | "2L-2R": "4ch-dright" 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Docs/pix/AdvancedChannelStrip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tv2/sisyfos-audio-controller/a7ad4618878197486a49e59aafc492182e8761a5/Docs/pix/AdvancedChannelStrip.png -------------------------------------------------------------------------------- /Docs/pix/SisyfosChannelStripdescription.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tv2/sisyfos-audio-controller/a7ad4618878197486a49e59aafc492182e8761a5/Docs/pix/SisyfosChannelStripdescription.jpg -------------------------------------------------------------------------------- /Docs/pix/SisyfosChanneldescription.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tv2/sisyfos-audio-controller/a7ad4618878197486a49e59aafc492182e8761a5/Docs/pix/SisyfosChanneldescription.jpg -------------------------------------------------------------------------------- /Docs/pix/mic-tally-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tv2/sisyfos-audio-controller/a7ad4618878197486a49e59aafc492182e8761a5/Docs/pix/mic-tally-view.png -------------------------------------------------------------------------------- /Docs/pix/minimonitorview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tv2/sisyfos-audio-controller/a7ad4618878197486a49e59aafc492182e8761a5/Docs/pix/minimonitorview.png -------------------------------------------------------------------------------- /Docs/pix/sisyfos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tv2/sisyfos-audio-controller/a7ad4618878197486a49e59aafc492182e8761a5/Docs/pix/sisyfos.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Producers Audio Mixer: Copyright (c) TV2 Denmark 4 | Contributions: Copyright (c) 2019 Norsk rikskringkasting AS 5 | Boilerplate: Copyright (c) Alex Devero (alexdevero.com) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | Cogwheel icon by Freepik from www.flaticon.com is licensed by CC 3.0 BY -------------------------------------------------------------------------------- /client/@types/react-slider/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-slider' -------------------------------------------------------------------------------- /client/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tv2/sisyfos-audio-controller/a7ad4618878197486a49e59aafc492182e8761a5/client/favicon/favicon.ico -------------------------------------------------------------------------------- /client/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SISYFOS INTELLIGENT AUDIO CONTROLLER 7 | 13 | 14 | 15 |
16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDom from 'react-dom' 3 | import App from './src/components/App' 4 | import { socketClientHandlers } from './src/utils/SocketClientHandlers' 5 | import io from 'socket.io-client' 6 | 7 | //Redux: 8 | import storeRedux from '../shared/src/reducers/store' 9 | import { Provider as ReduxProvider } from 'react-redux' 10 | import { 11 | SOCKET_GET_SNAPSHOT_LIST, 12 | SOCKET_GET_CCG_LIST, 13 | SOCKET_GET_MIXER_PRESET_LIST, 14 | SOCKET_GET_PAGES_LIST, 15 | } from '../shared/src/constants/SOCKET_IO_DISPATCHERS' 16 | 17 | import { I18nextProvider } from 'react-i18next' 18 | import i18n from './src/utils/i18n' 19 | import { IMixerProtocol } from '../shared/src/constants/MixerProtocolInterface' 20 | 21 | declare global { 22 | interface Window { 23 | storeRedux: any 24 | reduxState: any 25 | mixerProtocol: IMixerProtocol 26 | mixerProtocolPresets: any 27 | mixerProtocolList: any 28 | socketIoClient: any 29 | socketIoVuClient: any 30 | snapshotFileList: string[] 31 | ccgFileList: string[] 32 | mixerPresetList: string[] 33 | } 34 | } 35 | 36 | // *** Uncomment to log Socket I/O: 37 | // localStorage.debug = 'socket.io-client:socket'; 38 | 39 | window.storeRedux = storeRedux 40 | 41 | //Subscribe to redux store: 42 | window.reduxState = window.storeRedux.getState() 43 | const unsubscribe = window.storeRedux.subscribe(() => { 44 | window.reduxState = window.storeRedux.getState() 45 | }) 46 | 47 | window.socketIoClient = io() 48 | window.socketIoClient.emit(SOCKET_GET_SNAPSHOT_LIST) 49 | window.socketIoClient.emit(SOCKET_GET_CCG_LIST) 50 | window.socketIoClient.emit(SOCKET_GET_MIXER_PRESET_LIST) 51 | window.socketIoClient.emit(SOCKET_GET_PAGES_LIST) 52 | 53 | console.log('Setting up SocketIO connection') 54 | socketClientHandlers() 55 | window.socketIoClient.emit('get-store', 'update local store') 56 | window.socketIoClient.emit('get-settings', 'update local settings') 57 | window.socketIoClient.emit('get-mixerprotocol', 'get selected mixerprotocol') 58 | 59 | ReactDom.render( 60 | 61 | 62 | 63 | 64 | , 65 | document.getElementById('root') 66 | ) 67 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "webpack --config webpack.config.js --mode production", 7 | "watch": "webpack --config webpack.config.js --mode development --watch", 8 | "test": "echo \"Everything is fine\"", 9 | "test:watch": "echo \"Everything is fine and not watching\"" 10 | }, 11 | "devDependencies": { 12 | "@types/node": "^18.16.0", 13 | "@types/react-dom": "^18.2.4", 14 | "@types/react-test-renderer": "^18.0.0", 15 | "@babel/core": "^7.22.10", 16 | "css-loader": "^6.7.3", 17 | "html-webpack-plugin": "^5.5.1", 18 | "react-test-renderer": "^18.2.0", 19 | "style-loader": "^3.3.2", 20 | "ts-loader": "^9.4.2", 21 | "typescript": "^5.0.4", 22 | "webpack": "^5.82.1", 23 | "webpack-cli": "^5.1.1", 24 | "classnames": "^2.3.2", 25 | "i18next": "^20.6.1", 26 | "i18next-browser-languagedetector": "^7.0.1", 27 | "nouislider": "^15.7.0", 28 | "nouislider-react": "^3.4.1", 29 | "prop-types": "^15.8.1", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "react-draggable": "^4.4.5", 33 | "react-i18next": "^12.2.2", 34 | "react-redux": "^8.0.5", 35 | "react-select": "^5.7.3", 36 | "react-slider": "^2.0.4", 37 | "reactjs-popup": "^2.0.5", 38 | "redux": "^4.2.1", 39 | "socket.io-client": "^4.6.2", 40 | "webmidi": "^2.5.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/src/assets/css/App.css: -------------------------------------------------------------------------------- 1 | /* Main CSS file */ 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | font-family: sans-serif; 6 | background-color: black; 7 | user-select: none; 8 | overscroll-behavior: contain; 9 | } 10 | 11 | .App { 12 | position: relative; 13 | text-align: center; 14 | } 15 | 16 | .server-offline { 17 | position: absolute; 18 | top: 30vh; 19 | left: 30vw; 20 | margin-right: 30vw; 21 | color: aliceblue; 22 | background-color: rgba(163, 1, 1, 0.65); 23 | font-size: 400%; 24 | border-radius: 8px; 25 | border-width: 4px; 26 | border-color: rgba(144, 0, 0, 0.596); 27 | z-index: 200; 28 | } 29 | -------------------------------------------------------------------------------- /client/src/assets/css/CcgChannelSettings.css: -------------------------------------------------------------------------------- 1 | .channel-settings-body { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | width: 400px; 7 | overflow: auto; 8 | max-height: 90vh; 9 | background-color: rgb(31, 31, 31); 10 | border-color: rgb(124, 124, 124); 11 | border-style: solid; 12 | border-width: 4px; 13 | text-align: center; 14 | z-index: 2; 15 | 16 | color: #fff; 17 | } 18 | 19 | .channel-settings-body > h2 { 20 | border-bottom: 1px solid #999; 21 | margin: 0; 22 | padding: 10px 0; 23 | line-height: 50px; 24 | } 25 | 26 | .channel-settings-body > .close { 27 | position: absolute; 28 | outline : none; 29 | border-color: rgb(99, 99, 99); 30 | background-color: rgb(27, 27, 27); 31 | border-radius: 100%; 32 | display: block; 33 | color: #fff; 34 | width: 50px; 35 | height: 50px; 36 | font-size: 30px; 37 | line-height: 50px; 38 | top: 9px; 39 | right: 9px; 40 | } 41 | 42 | .channel-settings-group { 43 | border-bottom: 1px solid #999; 44 | } 45 | 46 | .channel-settings-group > button { 47 | line-height: 10px; 48 | 49 | outline : none; 50 | border-color: rgb(99, 99, 99); 51 | background-color: rgb(27, 27, 27); 52 | margin-right: auto; 53 | margin-top: 15px; 54 | margin-bottom: 15px; 55 | margin-left: auto; 56 | border-radius: 7px; 57 | display: block; 58 | color: #fff; 59 | width: 90%; 60 | font-size: 30px; 61 | line-height: 50px; 62 | } 63 | 64 | .channel-settings-group > button.active { 65 | border-color: rgb(17, 0, 255); 66 | } -------------------------------------------------------------------------------- /client/src/assets/css/ChanStrip.css: -------------------------------------------------------------------------------- 1 | .chan-strip-body { 2 | position: relative; 3 | top: 5px; 4 | width: 500px; 5 | height: 940px; 6 | background-color: rgb(31, 31, 31); 7 | border-color: rgb(70, 70, 70); 8 | border-style: solid; 9 | border-width: 4px; 10 | border-radius: 8px; 11 | text-align: left; 12 | color: #fff; 13 | } 14 | 15 | .chan-strip-body > .header { 16 | margin-left: 40px; 17 | margin-top: 10px; 18 | margin-right: 80px; 19 | /* width: 420px; */ 20 | font-size: 240%; 21 | color: #fff; 22 | white-space: nowrap; 23 | } 24 | 25 | .chan-strip-body > .parameters { 26 | top: 5px; 27 | left: 2px; 28 | height: 900px; 29 | overflow: auto; 30 | text-align: center; 31 | color: #fff; 32 | } 33 | 34 | .chan-strip-body .parameters .horizontal { 35 | display: flex; 36 | justify-content: space-evenly; 37 | } 38 | .chan-strip-body .parameters .horizontal .item { 39 | } 40 | .chan-strip-body .parameters .horizontal .item .content { 41 | display: flex; 42 | } 43 | 44 | .parameters > .inp-comp-del-group { 45 | display: flex; 46 | top: 5px; 47 | left: 2px; 48 | margin-left: 5px; 49 | overflow: auto; 50 | text-align: center; 51 | color: #fff; 52 | } 53 | 54 | .inp-comp-del-group > .horizontal-space { 55 | width: 40px; 56 | height: 50px; 57 | } 58 | 59 | .parameters > .eq-group, 60 | .content > .eq-group { 61 | display: flex; 62 | top: 5px; 63 | left: 2px; 64 | margin-left: 90px; 65 | overflow: auto; 66 | text-align: center; 67 | font-size: 14px; 68 | color: #fff; 69 | } 70 | .content > .eq-group { 71 | margin-left: 0; 72 | } 73 | 74 | .eq-group > .horizontal-space { 75 | width: 150px; 76 | height: 50px; 77 | } 78 | 79 | .eq > .group-text { 80 | text-align: center; 81 | line-height: 20px; 82 | font-size: 110%; 83 | } 84 | 85 | .zero-eq { 86 | width: 63px; 87 | height: 2px; 88 | margin-left: -68px; 89 | margin-right: 30px; 90 | margin-top: 111px; 91 | } 92 | 93 | .zero-comp { 94 | width: 63px; 95 | height: 2px; 96 | margin-left: -68px; 97 | margin-right: 30px; 98 | margin-top: 81px; 99 | } 100 | 101 | .zero-monitor { 102 | width: 2px; 103 | height: 20px; 104 | margin-left: 10px; 105 | margin-top: -150px; 106 | } 107 | 108 | .parameters > .parameter-text, 109 | .content > .parameter-text { 110 | list-style-type: none; 111 | text-align: center; 112 | margin-top: 15px; 113 | line-height: 10px; 114 | font-size: 14px; 115 | } 116 | 117 | .parameter-text > .parameter-mini-text, 118 | .content > .parameter-mini-text { 119 | list-style-type: none; 120 | text-align: center; 121 | margin-top: 5px; 122 | line-height: 10px; 123 | font-size: 80%; 124 | } 125 | 126 | .inp-comp-del-group > .delayButtons { 127 | width: 30px; 128 | margin-top: 24px; 129 | margin-left: 50px; 130 | } 131 | .content > .delayButtons { 132 | width: 30px; 133 | margin-top: 24px; 134 | margin-left: 50px; 135 | } 136 | 137 | .delayButtons > .delayTime { 138 | outline: none; 139 | -moz-outline: none; 140 | color: white; 141 | height: 45px; 142 | width: 70px; 143 | border-color: rgb(71, 71, 71); 144 | background-color: rgb(53, 53, 53); 145 | margin-left: -50px; 146 | margin-top: 5px; 147 | border-radius: 7px; 148 | } 149 | 150 | .inp-comp-del-group > .input-buttons { 151 | width: 30px; 152 | margin-top: 24px; 153 | margin-left: 50px; 154 | } 155 | .inp-comp-del-group .input-buttons.disabled { 156 | visibility: hidden; 157 | } 158 | .content > .input-buttons { 159 | width: 30px; 160 | margin-top: 24px; 161 | margin-left: 50px; 162 | } 163 | .content > .input-buttons.disabled { 164 | visibility: hidden; 165 | } 166 | 167 | .input-buttons > .input-select { 168 | outline: none; 169 | -moz-outline: none; 170 | color: white; 171 | height: 62px; 172 | width: 70px; 173 | border-color: rgb(71, 71, 71); 174 | background-color: rgb(53, 53, 53); 175 | margin-left: -50px; 176 | margin-top: 5px; 177 | border-radius: 7px; 178 | } 179 | .input-buttons > .input-select.active { 180 | background-color: #2f475b; 181 | } 182 | 183 | .parameters > .monitor-sends, 184 | .content > .monitor-sends { 185 | list-style-type: none; 186 | display: flex; 187 | text-align: center; 188 | margin-top: 15px; 189 | margin-left: 1px; 190 | padding: 0px; 191 | line-height: 10px; 192 | font-size: 95%; 193 | } 194 | 195 | .header > .close { 196 | position: absolute; 197 | outline: none; 198 | border-color: rgb(99, 99, 99); 199 | background-color: rgb(27, 27, 27); 200 | border-radius: 20px; 201 | color: #fff; 202 | width: 50px; 203 | height: 50px; 204 | font-size: 30px; 205 | /* margin-top: 5px; */ 206 | right: 10px; 207 | } 208 | 209 | .header > .button { 210 | outline: none; 211 | border-color: rgb(99, 99, 99); 212 | background-color: rgb(27, 27, 27); 213 | border-radius: 7px; 214 | margin-left: 10px; 215 | width: 80px; 216 | height: 50px; 217 | font-size: 12px; 218 | line-height: 30px; 219 | color: #fff; 220 | } 221 | 222 | .chan-strip-fader { 223 | width: 10px; 224 | height: 190px; 225 | margin-left: 35px; 226 | margin-right: 30px; 227 | margin-top: 10px; 228 | border-width: 0px; 229 | border-style: solid; 230 | border-radius: 8px; 231 | background-color: rgb(17, 17, 17); 232 | } 233 | 234 | .chan-strip-thumb { 235 | margin-left: -20px; 236 | color: #3a3a3a; 237 | font-size: 90%; 238 | line-height: 50px; 239 | width: 45px; 240 | height: 49px; 241 | border: 1px solid #c5c2c2; 242 | border-radius: 8px; 243 | background: linear-gradient( 244 | to top, 245 | #3a3a3a 0%, 246 | #c2c2c2 20%, 247 | hsl(0, 0%, 57%) 50%, 248 | #00a 1px, 249 | #919191 52%, 250 | #c2c2c2 80%, 251 | #3a3a3a 100% 252 | ); 253 | } 254 | -------------------------------------------------------------------------------- /client/src/assets/css/ChanStripEq.css: -------------------------------------------------------------------------------- 1 | .eq-full { 2 | justify-content: space-evenly; 3 | } 4 | 5 | .eq-full > .eq-text { 6 | display: flex; 7 | justify-content: space-evenly; 8 | top: 2vh; 9 | left: 2vw; 10 | width: 75vw; 11 | height: 10vh; 12 | margin-left: 1vw; 13 | margin-top: 2vh; 14 | } 15 | 16 | .eq-text-parameters { 17 | max-width: 22vw; 18 | } 19 | 20 | .eq-full > .eq-window { 21 | top: 2vh; 22 | left: 2vw; 23 | margin-top: 3vh; 24 | margin-left: 4vw; 25 | height: 30vh; 26 | width: 0vw; 27 | } 28 | 29 | .eq-full > .eq-canvas { 30 | position: absolute; 31 | margin-left: -36.5vw; 32 | margin-top: 5vh; 33 | height: 30vh; 34 | width: 72vw; 35 | } 36 | 37 | .eq-window > .dot { 38 | position: absolute; 39 | font-size: 5vh; 40 | height: 10vh; 41 | } 42 | 43 | .chstrip-q { 44 | width: 15vw; 45 | height: 0.5vh; 46 | margin-left: 5vw; 47 | margin-right: 5vw; 48 | margin-top: 1vh; 49 | border-width: 0px; 50 | border-style: solid; 51 | border-radius: 8px; 52 | background-color: rgb(17, 17, 17); 53 | } 54 | .chstrip-q-thumb { 55 | margin-top: -2vh; 56 | color: #3a3a3a; 57 | font-size: 0.8vh; 58 | width: 3vw; 59 | height: 4vh; 60 | line-height: 4vh; 61 | border: 1px solid #c5c2c2; 62 | border-radius: 8px; 63 | background: linear-gradient( 64 | to right, 65 | #3a3a3a 0%, 66 | #c2c2c2 20%, 67 | hsl(0, 0%, 57%) 50%, 68 | #00a 1px, 69 | #919191 52%, 70 | #c2c2c2 80%, 71 | #3a3a3a 100% 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /client/src/assets/css/ChanStripFull.css: -------------------------------------------------------------------------------- 1 | .chstrip-full-body { 2 | justify-content: space-evenly; 3 | position: fixed; 4 | top: 5vh; 5 | left: 5vw; 6 | width: 90vw; 7 | height: 90vh; 8 | overflow: auto; 9 | background-color: rgb(36, 36, 36); 10 | border-color: rgb(149, 149, 149); 11 | border-style: solid; 12 | border-width: 4px; 13 | border-radius: 8px; 14 | text-align: left; 15 | z-index: 2; 16 | color: #fff; 17 | } 18 | 19 | .ch-strip-full-header { 20 | margin-left: 5vw; 21 | margin-top: 1vh; 22 | margin-right: 10vw; 23 | /* width: 420px; */ 24 | font-size: 3vh; 25 | white-space: nowrap; 26 | } 27 | 28 | .chstrip-full-parameters { 29 | display: flex; 30 | justify-content: left; 31 | top: 2vh; 32 | left: 2vw; 33 | height: 30vh; 34 | overflow: auto; 35 | font-size: 1.5vh; 36 | text-align: center; 37 | } 38 | 39 | .chstrip-full-eq-window { 40 | display: flex; 41 | justify-content: space-evenly; 42 | font-size: 1.5vh; 43 | text-align: center; 44 | } 45 | 46 | .chstrip-full-parameter-text { 47 | list-style-type: none; 48 | text-align: center; 49 | margin-top: 2vh; 50 | font-size: 1.5vh; 51 | } 52 | 53 | .chstrip-full-content-group { 54 | display: flex; 55 | flex-direction: column; 56 | margin-right: 2vw; 57 | } 58 | 59 | .chstrip-full-content { 60 | display: flex; 61 | flex-direction: row; 62 | } 63 | 64 | .chstrip-full-parameter-reduction { 65 | list-style-type: none; 66 | text-align: center; 67 | margin-top: 2vh; 68 | font-size: 1.5vh; 69 | margin-left: 3vw; 70 | } 71 | 72 | .chstrip-full-mini-text { 73 | list-style-type: none; 74 | text-align: center; 75 | max-width: 9vw; 76 | margin-top: 1vh; 77 | font-size: 0.8vh; 78 | } 79 | 80 | .chstrip-full-zero-comp { 81 | width: 1vw; 82 | height: 0.2vh; 83 | margin-left: -6.5vw; 84 | margin-top: 15vh; 85 | font-size: 0.8vw; 86 | } 87 | 88 | .chstrip-full-zero-monitor { 89 | width: 1vw; 90 | height: 0.2vh; 91 | margin-left: 3.5vw; 92 | margin-top: -12vh; 93 | } 94 | 95 | .chstrip-full-parameter-text > .parameter-button-text { 96 | list-style-type: none; 97 | text-align: center; 98 | margin-top: 2vh; 99 | font-size: 1.2vh; 100 | } 101 | 102 | .chstrip-full-parameter-text > .parameter-button { 103 | margin-left: -8vw; 104 | margin-top: 2vh; 105 | outline: none; 106 | -moz-outline: none; 107 | margin-left: 3px; 108 | margin-right: 3px; 109 | color: white; 110 | height: 4vh; 111 | width: 4vw; 112 | font-size: 1.2vh; 113 | background-color: rgb(80, 80, 80); 114 | border-radius: 7px; 115 | border-color: rgb(99, 99, 99); 116 | } 117 | 118 | .chstrip-full-delay-buttons { 119 | margin-top: 3vh; 120 | margin-left: 1vw; 121 | } 122 | .noDelayButtons { 123 | margin-top: 24px; 124 | min-height: 220px; 125 | } 126 | 127 | .chstrip-full-delay-buttons > .delayTime { 128 | outline: none; 129 | -moz-outline: none; 130 | color: white; 131 | height: 5vh; 132 | width: 4vw; 133 | border-color: rgb(71, 71, 71); 134 | background-color: rgb(53, 53, 53); 135 | margin-left: 0vw; 136 | margin-top: 1vh; 137 | border-radius: 7px; 138 | } 139 | 140 | .chstrip-full-content > .input-buttons { 141 | width: 30px; 142 | margin-top: 24px; 143 | margin-left: 50px; 144 | } 145 | .chstrip-full-content > .input-buttons.disabled { 146 | visibility: hidden; 147 | } 148 | 149 | .input-buttons > .input-select { 150 | outline: none; 151 | -moz-outline: none; 152 | color: white; 153 | height: 62px; 154 | width: 70px; 155 | border-color: rgb(71, 71, 71); 156 | background-color: rgb(53, 53, 53); 157 | margin-left: -50px; 158 | margin-top: 5px; 159 | border-radius: 7px; 160 | } 161 | .input-buttons > .input-select.active { 162 | background-color: #2f475b; 163 | } 164 | 165 | .chstrip-full-monitor-sends { 166 | list-style-type: none; 167 | display: flex; 168 | text-align: center; 169 | margin-top: 2vh; 170 | min-width: 20vw; 171 | } 172 | 173 | .chstrip-full-monitor-text { 174 | list-style-type: none; 175 | text-align: center; 176 | margin-top: 2vh; 177 | font-size: 1.5vh; 178 | margin-left: -5vw; 179 | } 180 | 181 | .ch-strip-full-header > .close { 182 | position: absolute; 183 | outline: none; 184 | border-color: rgb(99, 99, 99); 185 | background-color: rgb(27, 27, 27); 186 | border-radius: 20px; 187 | color: #fff; 188 | width: 5vh; 189 | height: 5vh; 190 | font-size: 2vw; 191 | right: 3vw; 192 | } 193 | 194 | .ch-strip-full-header > .button { 195 | outline: none; 196 | border-color: rgb(99, 99, 99); 197 | background-color: rgb(27, 27, 27); 198 | border-radius: 7px; 199 | margin-left: 3vw; 200 | width: 10vw; 201 | height: 4vh; 202 | font-size: 0.8vw; 203 | } 204 | 205 | .chstrip-full-fader { 206 | width: 0.5vw; 207 | height: 15vh; 208 | margin-left: 5vw; 209 | margin-right: 5vw; 210 | margin-top: 3vh; 211 | border-width: 0px; 212 | border-style: solid; 213 | border-radius: 8px; 214 | background-color: rgb(17, 17, 17); 215 | } 216 | .chstrip-full-thumb { 217 | margin-left: -1.2vw; 218 | color: #3a3a3a; 219 | font-size: 1vh; 220 | line-height: 4vh; 221 | width: 3vw; 222 | height: 4vh; 223 | border: 1px solid #c5c2c2; 224 | border-radius: 0.4vw; 225 | background: linear-gradient( 226 | to top, 227 | #3a3a3a 0%, 228 | #c2c2c2 20%, 229 | hsl(0, 0%, 57%) 50%, 230 | #00a 1px, 231 | #919191 52%, 232 | #c2c2c2 80%, 233 | #3a3a3a 100% 234 | ); 235 | } 236 | -------------------------------------------------------------------------------- /client/src/assets/css/ChannelMonitorOptions.css: -------------------------------------------------------------------------------- 1 | .channel-monitor-body { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | width: 400px; 7 | overflow: auto; 8 | max-height: 90vh; 9 | background-color: rgb(31, 31, 31); 10 | border-color: rgb(124, 124, 124); 11 | border-style: solid; 12 | border-width: 4px; 13 | text-align: left; 14 | z-index: 2; 15 | 16 | color: #fff; 17 | } 18 | 19 | .channel-monitor-body > h2 { 20 | border-bottom: 1px solid #999; 21 | margin: 0; 22 | padding: 10px 0; 23 | line-height: 50px; 24 | text-align: center; 25 | } 26 | 27 | .channel-monitor-body > .input { 28 | white-space: nowrap; 29 | width: 20px; 30 | margin-left: 25px; 31 | color: rgb(255, 255, 255); 32 | } 33 | 34 | .channel-monitor-body > .input-field { 35 | text-align: right; 36 | margin: 5px; 37 | width: 20px; 38 | font-size: 1.1em; 39 | border: 1px solid rgb(104, 104, 104); 40 | background-color: rgb(87, 86, 86); 41 | color: white; 42 | border-radius: 0; 43 | -webkit-appearance: none; 44 | } 45 | 46 | .channel-monitor-text { 47 | color: dimgray; 48 | margin: 0; 49 | margin-left: 40px; 50 | padding: 0px 0; 51 | line-height: 20px; 52 | text-align: left; 53 | font-size: 110%; 54 | } 55 | 56 | .channel-monitor-text.checked { 57 | font-weight: bold; 58 | color: white; 59 | } 60 | 61 | .channel-monitor-body > .close { 62 | position: absolute; 63 | outline : none; 64 | border-color: rgb(99, 99, 99); 65 | background-color: rgb(27, 27, 27); 66 | border-radius: 100%; 67 | display: block; 68 | color: #fff; 69 | width: 50px; 70 | height: 50px; 71 | font-size: 30px; 72 | line-height: 50px; 73 | top: -5px; 74 | right: 9px; 75 | } 76 | 77 | .channel-monitor-body > button { 78 | line-height: 10px; 79 | outline : none; 80 | border-color: rgb(99, 99, 99); 81 | background-color: rgb(27, 27, 27); 82 | margin-right: 10px; 83 | margin-top: 15px; 84 | margin-bottom: 15px; 85 | margin-left: 10px; 86 | border-radius: 7px; 87 | color: #fff; 88 | width: 34%; 89 | font-size: 10px; 90 | line-height: 50px; 91 | } 92 | -------------------------------------------------------------------------------- /client/src/assets/css/ChannelRouteSettings.css: -------------------------------------------------------------------------------- 1 | .channel-route-body { 2 | position: fixed; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | width: 400px; 7 | overflow: auto; 8 | max-height: 90vh; 9 | background-color: rgb(31, 31, 31); 10 | border-color: rgb(124, 124, 124); 11 | border-style: solid; 12 | border-width: 4px; 13 | text-align: left; 14 | z-index: 2; 15 | 16 | color: #fff; 17 | } 18 | 19 | .channel-route-body > h2 { 20 | border-bottom: 1px solid #999; 21 | margin: 0; 22 | padding: 10px 0; 23 | line-height: 50px; 24 | text-align: center; 25 | } 26 | 27 | .channel-route-mixer-name { 28 | font-size: 150%; 29 | border-bottom: 1px solid #999; 30 | margin: 0; 31 | padding: 10px 0; 32 | line-height: 50px; 33 | text-align: center; 34 | } 35 | 36 | .channel-route-text { 37 | color: dimgray; 38 | margin: 0; 39 | margin-left: 40px; 40 | padding: 0px 0; 41 | line-height: 20px; 42 | text-align: left; 43 | font-size: 110%; 44 | } 45 | 46 | .channel-route-text.checked { 47 | font-weight: bold; 48 | color: white; 49 | } 50 | 51 | .channel-route-body > .close { 52 | position: absolute; 53 | outline: none; 54 | border-color: rgb(99, 99, 99); 55 | background-color: rgb(27, 27, 27); 56 | border-radius: 100%; 57 | display: block; 58 | color: #fff; 59 | width: 50px; 60 | height: 50px; 61 | font-size: 30px; 62 | line-height: 50px; 63 | top: -5px; 64 | right: 9px; 65 | } 66 | 67 | .channel-route-body > button { 68 | line-height: 10px; 69 | outline: none; 70 | border-color: rgb(99, 99, 99); 71 | background-color: rgb(27, 27, 27); 72 | margin-right: 10px; 73 | margin-top: 15px; 74 | margin-bottom: 15px; 75 | margin-left: 10px; 76 | border-radius: 7px; 77 | color: #fff; 78 | width: 34%; 79 | font-size: 10px; 80 | line-height: 50px; 81 | } 82 | -------------------------------------------------------------------------------- /client/src/assets/css/Channels.css: -------------------------------------------------------------------------------- 1 | .channels-body { 2 | display: flex; 3 | flex-direction: row; 4 | background-color: black; 5 | min-height: 760px; 6 | } 7 | 8 | .channels-body > .channels-body-inner { 9 | display: flex; 10 | flex-direction: row; 11 | flex-grow: 2; 12 | overflow-x: auto; 13 | height: calc(100vh); 14 | } 15 | 16 | .channels-body > .closedChanStrip { 17 | /* margin-left: -630px; */ 18 | transition: transform 300ms; 19 | position: absolute; 20 | z-index: 2; 21 | transform: translateX(-100%); 22 | } 23 | 24 | .channels-body > .openChanStrip { 25 | /* margin-left: 0px; 26 | transition: margin 800ms; */ 27 | transition: transform 300ms; 28 | transform: unset; 29 | } 30 | 31 | .button { 32 | outline: none; 33 | -moz-outline: none; 34 | color: white; 35 | height: 90px; 36 | width: 90px; 37 | border-color: rgb(71, 71, 71); 38 | background-color: rgb(53, 53, 53); 39 | margin-left: 5px; 40 | margin-right: 4px; 41 | margin-top: 10px; 42 | border-radius: 7px; 43 | } 44 | .button.half { 45 | height: 60px; 46 | } 47 | .button.active { 48 | background-color: #2f475b; 49 | } 50 | 51 | .button-all-manual.all { 52 | background-color: #af39b9; 53 | } 54 | .button-all-manual.any { 55 | border-color: #af39b9; 56 | } 57 | 58 | .channels-mix-body { 59 | display: flex; 60 | flex-direction: column; 61 | 62 | width: 100px; 63 | min-height: 950px; 64 | max-height: calc(100vh - 10px); 65 | color: white; 66 | background: linear-gradient( 67 | #2f2f2f 0px, 68 | #2f2f2f 790px, 69 | rgb(0, 0, 0) 1px, 70 | #2f2f2f 800px, 71 | #2f2f2f 100% 72 | ); 73 | margin: 4px; 74 | border-radius: 9px; 75 | border-color: rgb(80, 80, 80); 76 | border-style: solid; 77 | border-width: 1px; 78 | } 79 | .channels-mix-body > .mid { 80 | flex-grow: 2; 81 | display: flex; 82 | flex-direction: column; 83 | justify-content: center; 84 | } 85 | .channels-mix-body > .bot { 86 | overflow-y: auto; 87 | } 88 | 89 | .channels-show-mixer-online { 90 | background-color: rgb(219, 1, 1); 91 | } 92 | 93 | .channels-show-mixer-online.connected { 94 | background-color: rgb(9, 107, 0); 95 | } 96 | 97 | .channels-show-settings-button { 98 | } 99 | .channels-show-settings-button.active { 100 | background-color: #2f475b; 101 | } 102 | 103 | .channels-show-storage-button { 104 | } 105 | 106 | .channels-mix-button { 107 | background-color: rgb(65, 65, 65); 108 | } 109 | 110 | .channels-clear-button { 111 | background-color: rgb(19, 19, 19); 112 | } 113 | 114 | .channels-snap-mix-body { 115 | /* margin-top: 58px; */ 116 | } 117 | 118 | .channels-snap-mix-line { 119 | margin-top: 4px; 120 | background-color: #2b2b2b; 121 | } 122 | 123 | .channels-snap-mix-button { 124 | background-color: rgb(199, 202, 0); 125 | color: rgb(44, 44, 44); 126 | margin-left: 20px; 127 | height: 25px; 128 | width: 60px; 129 | border-color: rgb(99, 99, 99); 130 | white-space: nowrap; 131 | } 132 | 133 | .channels-snap-mix-line:last-of-type { 134 | margin-bottom: 51px; 135 | } 136 | -------------------------------------------------------------------------------- /client/src/assets/css/LabelSettings.css: -------------------------------------------------------------------------------- 1 | .label-settings-body { 2 | position: fixed; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | width: 600px; 7 | overflow: auto; 8 | max-height: 90vh; 9 | background-color: rgb(31, 31, 31); 10 | border-color: rgb(124, 124, 124); 11 | border-style: solid; 12 | border-width: 4px; 13 | text-align: center; 14 | align-items: center; 15 | z-index: 2; 16 | 17 | color: #fff; 18 | } 19 | 20 | .label-settings-body > h2 { 21 | border-bottom: 1px solid #999; 22 | margin: 0; 23 | padding: 10px 0; 24 | line-height: 50px; 25 | text-align: center; 26 | } 27 | 28 | .pages-settings-mixer-name { 29 | font-size: 150%; 30 | border-bottom: 1px solid #999; 31 | margin: 0; 32 | padding: 10px 0; 33 | line-height: 50px; 34 | text-align: center; 35 | } 36 | 37 | .pages-settings-text { 38 | color: dimgray; 39 | margin: 0; 40 | margin-left: 40px; 41 | padding: 0px 0; 42 | line-height: 20px; 43 | text-align: right; 44 | font-size: 110%; 45 | } 46 | 47 | .pages-settings-tick { 48 | color: dimgray; 49 | margin: 0; 50 | margin-right: 70px; 51 | padding: 0px 0; 52 | line-height: 20px; 53 | text-align: right; 54 | align-content: right; 55 | font-size: 110%; 56 | } 57 | 58 | .pages-settings.checked { 59 | font-weight: bold; 60 | color: white; 61 | } 62 | 63 | .label-settings-body > .close { 64 | position: absolute; 65 | outline: none; 66 | border-color: rgb(99, 99, 99); 67 | background-color: rgb(27, 27, 27); 68 | border-radius: 100%; 69 | display: block; 70 | color: #fff; 71 | width: 50px; 72 | height: 50px; 73 | font-size: 30px; 74 | line-height: 50px; 75 | top: -5px; 76 | right: 9px; 77 | } 78 | 79 | .label-settings-body > .button { 80 | line-height: 20px; 81 | outline: none; 82 | border-color: rgb(99, 99, 99); 83 | background-color: rgb(58, 3, 3); 84 | margin-top: 15px; 85 | margin-bottom: 15px; 86 | border-radius: 7px; 87 | color: #fff; 88 | width: 34%; 89 | height: 50px; 90 | font-size: 10px; 91 | } 92 | 93 | .label-settings-body > .inputfield { 94 | line-height: 40px; 95 | outline: none; 96 | margin-top: 45px; 97 | border-radius: 7px; 98 | color: #fff; 99 | width: 34%; 100 | height: 50px; 101 | font-size: 18px; 102 | } 103 | -------------------------------------------------------------------------------- /client/src/assets/css/MicTally.css: -------------------------------------------------------------------------------- 1 | body.v-mic-tally { 2 | background: transparent; 3 | min-height: 100%; 4 | } 5 | 6 | .mic-tally-view { 7 | position: relative; 8 | color: white; 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | .mic-tally-list { 14 | display: flex; 15 | list-style: none; 16 | align-items: stretch; 17 | flex-wrap: wrap; 18 | padding: 0; 19 | margin: 0; 20 | opacity: 0; 21 | transition: opacity 0.2s; 22 | } 23 | 24 | .mic-tally-list.active { 25 | opacity: 1; 26 | } 27 | 28 | .c-mic-tally { 29 | margin: 8px; 30 | flex: 1 1 80px; 31 | min-width: 80px; 32 | text-align: center; 33 | -webkit-user-select: none; 34 | -ms-user-select: none; 35 | user-select: none; 36 | } 37 | .c-mic-tally:not(:first-child) { 38 | margin-left: 8px; 39 | } 40 | .c-mic-tally__status { 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | font-weight: bold; 45 | border-radius: 100%; 46 | margin: 0 auto; 47 | background-color: #444444; 48 | border: 2px solid #888888; 49 | width: 60px; 50 | height: 60px; 51 | cursor: pointer; 52 | } 53 | 54 | .c-mic-tally__status:active:not(.muted) { 55 | background-color: #888888; 56 | } 57 | 58 | .c-mic-tally__status.on { 59 | color: #c40000; 60 | background-color: #ffaaaa; 61 | border-color: #c40000; 62 | font-size: 1em; 63 | } 64 | 65 | .c-mic-tally__status.on:active { 66 | background-color: #c40000; 67 | color: white; 68 | } 69 | 70 | .c-mic-tally__status.muted { 71 | cursor: not-allowed; 72 | background-color: transparent; 73 | border-color: #444444; 74 | } 75 | 76 | .c-mic-tally__label { 77 | display: block; 78 | color: white; 79 | font-weight: bold; 80 | margin-top: 6px; 81 | font-size: 0.9em; 82 | } 83 | .c-mic-tally__toggle-prompt { 84 | display: none; 85 | position: absolute; 86 | top: 0; 87 | left: 0; 88 | right: 0; 89 | bottom: 0; 90 | align-items: center; 91 | justify-content: center; 92 | z-index: 10000; 93 | } 94 | .c-mic-tally__toggle-prompt.active { 95 | display: flex; 96 | } 97 | 98 | .c-mic-tally__toggle-prompt__content { 99 | flex: 1; 100 | text-align: center; 101 | font-size: 18px; 102 | font-weight: bold; 103 | } 104 | .c-mic-tally__toggle-prompt__actions { 105 | display: flex; 106 | } 107 | 108 | .c-mic-tally__toggle-prompt__actions button { 109 | flex: 1; 110 | outline: none; 111 | -moz-outline: none; 112 | padding: 12px; 113 | margin: 8px; 114 | border: none; 115 | border-radius: 8px; 116 | font-size: 16px; 117 | font-weight: bold; 118 | color: white; 119 | background-color: #c40000; 120 | cursor: pointer; 121 | transition: background-color 0.2s, color 0.2s; 122 | } 123 | .c-mic-tally__toggle-prompt__actions button.cancel { 124 | background-color: rgb(66, 27, 27); 125 | } 126 | .c-mic-tally__toggle-prompt__actions button:hover, 127 | .c-mic-tally__toggle-prompt__actions button:active { 128 | background-color: #ffaaaa; 129 | color: rgb(66, 27, 27); 130 | } 131 | -------------------------------------------------------------------------------- /client/src/assets/css/MiniChanStrip.css: -------------------------------------------------------------------------------- 1 | .monitor-chan-strip-body { 2 | position: absolute; 3 | top: 2px; 4 | left: 100px; 5 | width: 620px; 6 | height: 280px; 7 | background-color: rgb(31, 31, 31); 8 | border-color: rgb(70, 70, 70); 9 | border-style: solid; 10 | border-width: 4px; 11 | border-radius: 8px; 12 | text-align: left; 13 | color: #fff; 14 | } 15 | 16 | .monitor-chan-strip-body > .header { 17 | margin-left: 40px; 18 | margin-top: 10px; 19 | width: 420px; 20 | font-size: 240%; 21 | color: #fff; 22 | } 23 | 24 | .monitor-chan-strip-body > .parameters { 25 | margin-top: 5px; 26 | top: 5px; 27 | left: 2px; 28 | height: 900px; 29 | overflow: auto; 30 | text-align: center; 31 | color: #fff; 32 | } 33 | 34 | .parameters > .parameter-group { 35 | display: flex; 36 | top: 5px; 37 | left: 2px; 38 | margin-left: 90px; 39 | overflow: auto; 40 | text-align: center; 41 | color: #fff; 42 | } 43 | 44 | .parameter-group > .horizontal-space { 45 | width: 150px; 46 | height: 50px; 47 | } 48 | 49 | .parameters > .group-text { 50 | text-align: center; 51 | line-height: 20px; 52 | font-size: 110%; 53 | } 54 | 55 | .zero-monitor { 56 | width: 2px; 57 | height: 20px; 58 | margin-left: 10px; 59 | margin-top: -150px; 60 | } 61 | 62 | .parameters > .parameter-text { 63 | list-style-type: none; 64 | text-align: center; 65 | margin-top: 15px; 66 | line-height: 10px; 67 | font-size: 100%; 68 | } 69 | 70 | .parameters > .monitor-sends { 71 | list-style-type: none; 72 | display: flex; 73 | text-align: center; 74 | margin-top: 15px; 75 | margin-left: 1px; 76 | padding: 0px; 77 | line-height: 10px; 78 | font-size: 95%; 79 | } 80 | 81 | .monitor-chan-strip-body .header > .close { 82 | position: fixed; 83 | outline: none; 84 | border-color: rgb(99, 99, 99); 85 | background-color: rgb(27, 27, 27); 86 | border-radius: 20px; 87 | color: #fff; 88 | width: 50px; 89 | height: 50px; 90 | font-size: 30px; 91 | margin-top: 5px; 92 | left: 550px; 93 | } 94 | 95 | .header > button { 96 | outline: none; 97 | border-color: rgb(99, 99, 99); 98 | background-color: rgb(27, 27, 27); 99 | border-radius: 7px; 100 | margin-left: 10px; 101 | margin-top: 5px; 102 | width: 180px; 103 | font-size: 12px; 104 | line-height: 40px; 105 | color: #fff; 106 | } 107 | 108 | .monitor-chan-strip-fader { 109 | width: 10px; 110 | height: 200px; 111 | margin-left: 35px; 112 | margin-right: 30px; 113 | margin-top: 10px; 114 | border-width: 0px; 115 | border-style: solid; 116 | border-radius: 8px; 117 | background-color: rgb(17, 17, 17); 118 | } 119 | 120 | .monitor-chan-strip-thumb { 121 | margin-left: -20px; 122 | width: 45px; 123 | height: 49px; 124 | border: 1px solid #c5c2c2; 125 | border-radius: 8px; 126 | background: linear-gradient( 127 | to top, 128 | #3a3a3a 0%, 129 | #c2c2c2 20%, 130 | hsl(0, 0%, 57%) 50%, 131 | #00a 1px, 132 | #919191 52%, 133 | #c2c2c2 80%, 134 | #3a3a3a 100% 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /client/src/assets/css/MiniChannel.css: -------------------------------------------------------------------------------- 1 | .monitor-channel-body { 2 | color: white; 3 | background: linear-gradient(#2f2f2f 0px,#2f2f2f 790px, rgb(0, 0, 0) 1px, #2f2f2f 800px, #2f2f2f 100%); 4 | margin: 4px; 5 | border-radius: 7px; 6 | width: 86px; 7 | height: 86px; 8 | position: relative; 9 | } 10 | 11 | .monitor-channel-strip-button { 12 | outline : none; 13 | -moz-outline : none; 14 | margin-left: 3px; 15 | margin-top: 5px; 16 | margin-right: 3px; 17 | color: white; 18 | height: 80px; 19 | width: 80px; 20 | font-size: 105%; 21 | background-color: rgba(39, 39, 39, 0.308); 22 | border-radius: 7px; 23 | border-color: rgba(77, 77, 77, 0.301); 24 | } 25 | 26 | .monitor-channel-strip-button.on { 27 | background-color: rgb(146, 146, 146); 28 | border-color: rgba(102, 102, 102, 0.301); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /client/src/assets/css/MiniChannels.css: -------------------------------------------------------------------------------- 1 | .mini-channels-body { 2 | display: flex; 3 | flex-direction: column; 4 | background-color: black; 5 | height: 400px; 6 | width: 100px; 7 | } 8 | 9 | .mini-channels-body > .closedChanStrip { 10 | margin-left: 100px; 11 | transition: margin 300ms; 12 | } 13 | 14 | .mini-channels-body > .openChanStrip { 15 | margin-left: 100px; 16 | transition: margin 800ms; 17 | } -------------------------------------------------------------------------------- /client/src/assets/css/PagesSettings.css: -------------------------------------------------------------------------------- 1 | .pages-settings-body { 2 | position: fixed; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | width: 400px; 7 | overflow: auto; 8 | max-height: 90vh; 9 | background-color: rgb(31, 31, 31); 10 | border-color: rgb(124, 124, 124); 11 | border-style: solid; 12 | border-width: 4px; 13 | text-align: center; 14 | align-items: center; 15 | z-index: 2; 16 | 17 | color: #fff; 18 | } 19 | 20 | .pages-settings-body > h2 { 21 | border-bottom: 1px solid #999; 22 | margin: 0; 23 | padding: 10px 0; 24 | line-height: 50px; 25 | text-align: center; 26 | } 27 | 28 | .pages-settings-mixer-name { 29 | font-size: 150%; 30 | border-bottom: 1px solid #999; 31 | margin: 0; 32 | padding: 10px 0; 33 | line-height: 50px; 34 | text-align: center; 35 | } 36 | 37 | .pages-settings-text { 38 | color: dimgray; 39 | margin: 0; 40 | margin-left: 40px; 41 | padding: 0px 0; 42 | line-height: 20px; 43 | text-align: right; 44 | font-size: 110%; 45 | } 46 | 47 | .pages-settings-tick { 48 | color: dimgray; 49 | margin: 0; 50 | margin-right: 70px; 51 | padding: 0px 0; 52 | line-height: 20px; 53 | text-align: right; 54 | align-content: right; 55 | font-size: 110%; 56 | } 57 | 58 | .pages-settings.checked { 59 | font-weight: bold; 60 | color: white; 61 | } 62 | 63 | .pages-settings-body > .close { 64 | position: absolute; 65 | outline: none; 66 | border-color: rgb(99, 99, 99); 67 | background-color: rgb(27, 27, 27); 68 | border-radius: 100%; 69 | display: block; 70 | color: #fff; 71 | width: 50px; 72 | height: 50px; 73 | font-size: 30px; 74 | line-height: 50px; 75 | top: -5px; 76 | right: 9px; 77 | } 78 | 79 | .pages-settings-body > .button { 80 | line-height: 20px; 81 | outline: none; 82 | border-color: rgb(99, 99, 99); 83 | background-color: rgb(58, 3, 3); 84 | margin-top: 15px; 85 | margin-bottom: 15px; 86 | margin-left: 130px; 87 | border-radius: 7px; 88 | color: #fff; 89 | width: 34%; 90 | height: 50px; 91 | font-size: 10px; 92 | } 93 | 94 | .pages-settings-body > .inputfield { 95 | line-height: 40px; 96 | outline: none; 97 | margin-top: 45px; 98 | border-radius: 7px; 99 | color: #fff; 100 | width: 34%; 101 | height: 50px; 102 | font-size: 18px; 103 | } 104 | -------------------------------------------------------------------------------- /client/src/assets/css/ReductionMeter.css: -------------------------------------------------------------------------------- 1 | .reductionmeter-body { 2 | top: 0.8vh; 3 | width: 24px; 4 | margin-top: 15px; 5 | margin-left: 5px; 6 | background-color: black; 7 | border-radius: 7px; 8 | font-size: 14px; 9 | } 10 | 11 | .reductionmeter-canvas { 12 | width: 10px; 13 | margin-left: -5px; 14 | bottom: 0px; 15 | height: 240x; 16 | } 17 | 18 | .reductionmeter-lower-part { 19 | width: 10px; 20 | margin-left: 10px; 21 | background-color: rgb(0, 122, 37); 22 | } 23 | .reductionmeter-middle-part { 24 | width: 10px; 25 | margin-left: 10px; 26 | background-color: rgb(53, 167, 0); 27 | } 28 | .reductionmeter-upper-part { 29 | width: 10px; 30 | margin-left: 10px; 31 | background-color: rgb(206, 0, 0); 32 | } 33 | 34 | .reduction-6db { 35 | color: #445f4a; 36 | width: 63px; 37 | height: 2px; 38 | margin-left: -6px; 39 | margin-top: -105px; 40 | } 41 | .reduction-12db { 42 | color: #5c3939; 43 | width: 63px; 44 | height: 2px; 45 | margin-left: 2px; 46 | margin-top: 50px; 47 | } 48 | -------------------------------------------------------------------------------- /client/src/assets/css/RoutingStorage.css: -------------------------------------------------------------------------------- 1 | .channel-storage-body { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | width: 400px; 7 | overflow: auto; 8 | max-height: 90vh; 9 | background-color: rgb(31, 31, 31); 10 | border-color: rgb(124, 124, 124); 11 | border-style: solid; 12 | border-width: 4px; 13 | text-align: left; 14 | z-index: 2; 15 | 16 | color: #fff; 17 | } 18 | 19 | .channel-storage-body > h2 { 20 | border-bottom: 1px solid #999; 21 | margin: 0; 22 | padding: 10px 0; 23 | line-height: 50px; 24 | text-align: center; 25 | 26 | } 27 | 28 | 29 | .channel-storage-body > h3 { 30 | margin: 0; 31 | padding: 0px 0; 32 | line-height: 20px; 33 | text-align: center; 34 | font-size: 110%; 35 | } 36 | 37 | .channel-storage-body > h4 { 38 | margin: 0; 39 | padding: 0px 0; 40 | line-height: 20px; 41 | text-align: center; 42 | font-size: 110%; 43 | } 44 | 45 | .channel-storage-body > .close { 46 | position: absolute; 47 | outline : none; 48 | border-color: rgb(99, 99, 99); 49 | background-color: rgb(27, 27, 27); 50 | border-radius: 100%; 51 | display: block; 52 | color: #fff; 53 | width: 50px; 54 | height: 50px; 55 | font-size: 30px; 56 | line-height: 50px; 57 | top: 1px; 58 | right: 9px; 59 | } 60 | 61 | .channel-storage-body > button { 62 | line-height: 10px; 63 | 64 | outline : none; 65 | border-color: rgb(99, 99, 99); 66 | background-color: rgb(97, 0, 0); 67 | margin-top: 15px; 68 | margin-bottom: 15px; 69 | margin-left: 120px; 70 | border-radius: 7px; 71 | display: block; 72 | color: #fff; 73 | width: 160px; 74 | font-size: 20px; 75 | line-height: 50px; 76 | } 77 | 78 | .channel-storage-group > button.active { 79 | border-color: rgb(17, 0, 255); 80 | } 81 | 82 | 83 | .storage-list { 84 | list-style-type:none; 85 | } 86 | 87 | .storage-list > .item { 88 | margin-bottom: 5px; 89 | margin-right: 40px; 90 | background-color: rgb(43, 42, 42); 91 | border-color: rgb(94, 94, 94); 92 | border-style: solid; 93 | border-width: 1px; 94 | border-radius: 7px; 95 | line-height: 30px; 96 | text-align: center; 97 | font-size: 120%; 98 | } -------------------------------------------------------------------------------- /client/src/assets/css/Settings.css: -------------------------------------------------------------------------------- 1 | .settings-body { 2 | position: fixed; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | overflow: auto; 7 | max-height: 90vh; 8 | width: 700px; 9 | background-color: rgb(31, 31, 31); 10 | border-color: rgb(124, 124, 124); 11 | border-style: solid; 12 | border-width: 4px; 13 | text-align: center; 14 | z-index: 2; 15 | } 16 | 17 | .settings-header{ 18 | margin-top: 20px; 19 | margin-bottom: 20px; 20 | color: white; 21 | font-size: 140%; 22 | border-width: 2px; 23 | border-color: black; 24 | border-style: solid; 25 | } 26 | 27 | 28 | .settings-items-body { 29 | margin-top: 10px; 30 | font-size: 1.1em; 31 | background-color: rgb(31, 31, 31); 32 | } 33 | 34 | .settings-input-field { 35 | white-space: nowrap; 36 | margin: 5px; 37 | color: rgb(255, 255, 255); 38 | } 39 | 40 | 41 | .settings-show-channel-selection { 42 | white-space: nowrap; 43 | margin: 5px; 44 | color: rgb(255, 255, 255); 45 | } 46 | 47 | input[type=text] { 48 | text-align: right; 49 | margin: 5px; 50 | font-size: 1.1em; 51 | border: 1px solid rgb(104, 104, 104); 52 | background-color: rgb(87, 86, 86); 53 | color: white; 54 | border-radius: 0; 55 | -webkit-appearance: none; 56 | } 57 | 58 | 59 | .settings-channels-button { 60 | margin-top: 10px; 61 | margin-bottom: 10px; 62 | font-size: 0.9em; 63 | width: 120px; 64 | line-height: 100%; 65 | background: rgb(13, 77, 18); 66 | color: white; 67 | font-family: 'Roboto Condensed', sans-serif; 68 | border-radius: 4px; 69 | border-width: 1; 70 | border-color: black; 71 | outline: 0; 72 | } 73 | 74 | .settings-save-button { 75 | margin-top: 10px; 76 | margin-bottom: 10px; 77 | left: calc(50% - 90px); 78 | font-size: 1.1em; 79 | width: 180px; 80 | line-height: 300%; 81 | background: rgb(43, 4, 4); 82 | color: white; 83 | font-family: 'Roboto Condensed', sans-serif; 84 | border-radius: 16px; 85 | border-width: 0; 86 | outline: 0; 87 | } 88 | 89 | .settings-cancel-button { 90 | margin-top: 10px; 91 | margin-right: 10px; 92 | margin-bottom: 10px; 93 | left: calc(50% - 90px); 94 | font-size: 1.1em; 95 | width: 180px; 96 | line-height: 300%; 97 | background: rgb(54, 54, 54); 98 | color: white; 99 | font-family: 'Roboto Condensed', sans-serif; 100 | border-radius: 16px; 101 | border-width: 0; 102 | outline: 0; 103 | } 104 | 105 | 106 | -------------------------------------------------------------------------------- /client/src/assets/css/VuMeter.css: -------------------------------------------------------------------------------- 1 | .vumeter-body { 2 | width: 24px; 3 | margin-left: 2px; 4 | margin-right: 4px; 5 | background-color: black; 6 | border-radius: 7px; 7 | height: 430px; 8 | } 9 | 10 | .vumeter-canvas { 11 | width: 10px; 12 | margin-left: 6px; 13 | bottom: 0; 14 | height: 100%; 15 | margin-top: 50%; 16 | } 17 | 18 | .vumeter-lower-part { 19 | position: absolute; 20 | width: 10px; 21 | margin-left: 10px; 22 | background-color: rgb(0, 122, 37); 23 | } 24 | .vumeter-middle-part { 25 | position: absolute; 26 | width: 10px; 27 | margin-left: 10px; 28 | background-color: rgb(53, 167, 0); 29 | } 30 | .vumeter-upper-part { 31 | position: absolute; 32 | width: 10px; 33 | margin-left: 10px; 34 | background-color: rgb(206, 0, 0); 35 | } 36 | -------------------------------------------------------------------------------- /client/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { connect } from 'react-redux' 3 | import { compose } from 'redux' 4 | import { IStore } from '../../../shared/src/reducers/store' 5 | 6 | import '../assets/css/App.css' 7 | import Channels from './Channels' 8 | import Settings from './Settings' 9 | import Storage from './RoutingStorage' 10 | import MiniChannels from './MiniChannels' 11 | import MicTally from './MicTally' 12 | import { withTranslation } from 'react-i18next' 13 | import PagesSettings from './PagesSettings' 14 | import LabelSettings from './Labels' 15 | 16 | export interface IAppProps { 17 | store: IStore 18 | t: any 19 | } 20 | 21 | class App extends React.Component { 22 | constructor(props: IAppProps) { 23 | super(props) 24 | } 25 | 26 | componentWillMount() { 27 | console.log( 28 | 'http args : ', 29 | window.location.search.includes('settings=1') 30 | ) 31 | window.socketIoClient.emit( 32 | 'get-mixerprotocol', 33 | 'get selected mixerprotocol' 34 | ) 35 | window.socketIoClient.emit('get-store', 'update local store') 36 | window.socketIoClient.emit('get-settings', 'update local settings') 37 | this.iFrameFocusHandler() 38 | this.contextMenuHandler() 39 | } 40 | 41 | public shouldComponentUpdate(nextProps: IAppProps) { 42 | return ( 43 | nextProps.store.settings[0].showSettings != 44 | this.props.store.settings[0].showSettings || 45 | nextProps.store.settings[0].showPagesSetup != 46 | this.props.store.settings[0].showPagesSetup || 47 | nextProps.store.settings[0].showLabelSettings != 48 | this.props.store.settings[0].showLabelSettings || 49 | nextProps.store.settings[0].serverOnline != 50 | this.props.store.settings[0].serverOnline || 51 | nextProps.store.settings[0].showStorage != 52 | this.props.store.settings[0].showStorage 53 | ) 54 | } 55 | 56 | sendSofieMessage(type: string, payload?: any | '', replyTo?: string | '') { 57 | window.top.postMessage( 58 | { 59 | id: Date.now().toString(), 60 | replyToId: replyTo, 61 | type: type, 62 | payload: payload, 63 | }, 64 | '*' 65 | ) 66 | } 67 | 68 | iFrameFocusHandler() { 69 | if (window.top !== window.self) { 70 | this.sendSofieMessage('hello') 71 | document.addEventListener( 72 | 'click', 73 | (e) => { 74 | e.preventDefault() 75 | this.sendSofieMessage('focus_in') 76 | }, 77 | true 78 | ) 79 | window.addEventListener('message', (event) => { 80 | try { 81 | const message = event.data 82 | if (!message || !message.type) return 83 | switch (message.type) { 84 | case 'welcome': 85 | console.log('Hosted by: ' + message.payload) 86 | // finish three-way handshake 87 | this.sendSofieMessage('ack', undefined, message.id) 88 | break 89 | } 90 | } catch (e) { 91 | console.log('Error Sofie API') 92 | } 93 | }) 94 | } 95 | } 96 | 97 | /** 98 | * disables context menu in order to enable multi touch support 99 | */ 100 | contextMenuHandler() { 101 | document.addEventListener( 102 | 'contextmenu', 103 | function (e) { 104 | e.preventDefault() 105 | }, 106 | false 107 | ) 108 | } 109 | 110 | render() { 111 | const urlParams = new URLSearchParams(window.location.search) 112 | const viewId = urlParams.get('view') 113 | return ( 114 |
115 | {!this.props.store.settings[0].serverOnline && ( 116 |
117 | {this.props.t('TRYING TO CONNECT TO SISYFOS SERVER')} 118 |
119 | )} 120 | { (viewId === 'minimonitor') ? ( 121 | 122 | ) : (viewId === 'mic-tally') ? ( 123 | 124 | ) : ( 125 | 126 | )} 127 | {this.props.store.settings[0].showLabelSettings && } 128 | {this.props.store.settings[0].showPagesSetup && } 129 | {this.props.store.settings[0].showStorage && } 130 | {this.props.store.settings[0].showSettings && } 131 |
132 | ) 133 | } 134 | } 135 | 136 | const mapStateToProps = (state: any, t: any): IAppProps => { 137 | return { 138 | store: state, 139 | t: t, 140 | } 141 | } 142 | 143 | export default compose( 144 | connect(mapStateToProps), 145 | withTranslation() 146 | )(App) as any 147 | -------------------------------------------------------------------------------- /client/src/components/Labels.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from 'react' 2 | 3 | import '../assets/css/LabelSettings.css' 4 | import { Store } from 'redux' 5 | import { connect } from 'react-redux' 6 | import { 7 | storeShowLabelSetup, 8 | } from '../../../shared/src/actions/settingsActions' 9 | import { IFader } from '../../../shared/src/reducers/fadersReducer' 10 | import { 11 | SOCKET_FLUSH_LABELS, 12 | SOCKET_SET_LABELS, 13 | } from '../../../shared/src/constants/SOCKET_IO_DISPATCHERS' 14 | import { ICustomPages } from '../../../shared/src/reducers/settingsReducer' 15 | import { getChannelLabel } from '../utils/labels' 16 | import { flushExtLabels, updateLabels } from '../../../shared/src/actions/faderActions' 17 | import { ChannelActionTypes, ChannelActions } from '../../../shared/src/actions/channelActions' 18 | import { Dispatch } from '@reduxjs/toolkit' 19 | 20 | interface ILabelSettingsInjectProps { 21 | customPages: ICustomPages[] 22 | fader: IFader[] 23 | } 24 | 25 | class LabelSettings extends React.PureComponent< 26 | ILabelSettingsInjectProps & Store 27 | > { 28 | state = { 29 | mutations: {} as Record 30 | } 31 | dispatch: Dispatch = this.props.dispatch 32 | 33 | constructor(props: any) { 34 | super(props) 35 | } 36 | 37 | componentDidMount() { 38 | this.setState({ label: this.props.customPages[0].label }) 39 | } 40 | 41 | handleLabel = (event: ChangeEvent, index: number) => { 42 | console.log(event.target.value, index) 43 | this.setState({ mutations: { ...this.state.mutations, [index]: event.target.value }}) 44 | } 45 | 46 | handleClearLabels() { 47 | if (window.confirm('Clear all user defined labels?')) { 48 | const faders = this.props.fader 49 | .map((f, i) => ({ label: f.userLabel, i })) 50 | .filter(f => f.label) 51 | .reduce((a, b) => ({ ...a, [b.i]: '' }), {} as Record) 52 | 53 | this.props.dispatch(updateLabels(faders)) 54 | this.props.dispatch(storeShowLabelSetup()) 55 | window.socketIoClient.emit(SOCKET_SET_LABELS, { update: faders }) 56 | } 57 | } 58 | 59 | handleFlushLabels() { 60 | if (window.confirm('Flush all external (automation and channel) labels?')) { 61 | this.props.dispatch(flushExtLabels()) 62 | this.dispatch({ type: ChannelActionTypes.FLUSH_CHANNEL_LABELS }) 63 | window.socketIoClient.emit(SOCKET_FLUSH_LABELS) 64 | } 65 | } 66 | 67 | handleClose = () => { 68 | // window.socketIoClient.emit(SOCKET_GET_PAGES_LIST) 69 | this.props.dispatch(storeShowLabelSetup()) 70 | } 71 | 72 | handleCancel = () => { 73 | // window.socketIoClient.emit(SOCKET_GET_PAGES_LIST) 74 | this.props.dispatch(storeShowLabelSetup()) 75 | } 76 | 77 | handleSave = () => { 78 | this.props.dispatch(updateLabels(this.state.mutations)) 79 | this.props.dispatch(storeShowLabelSetup()) 80 | window.socketIoClient.emit(SOCKET_SET_LABELS, { update: this.state.mutations }) 81 | } 82 | 83 | renderLabelList() { 84 | return ( 85 |
86 | {this.props.fader.map((fader: IFader, index: number) => { 87 | const faderLabel = fader.label || '-' 88 | const channelLabel = getChannelLabel(window.reduxState, index) || '-' 89 | const mutated = this.state.mutations[index] 90 | 91 | return ( 92 | 93 | 102 |
103 |
104 | ) 105 | })} 106 |
107 | ) 108 | } 109 | 110 | render() { 111 | return ( 112 |
113 |

CUSTOM LABELS

114 | 117 | {this.renderLabelList()} 118 | 124 | 130 |
131 | 139 | 147 |
148 | ) 149 | } 150 | } 151 | 152 | const mapStateToProps = (state: any, props: any): ILabelSettingsInjectProps => { 153 | return { 154 | customPages: state.settings[0].customPages, 155 | fader: state.faders[0].fader, 156 | } 157 | } 158 | 159 | export default connect(mapStateToProps)( 160 | LabelSettings 161 | ) as any 162 | -------------------------------------------------------------------------------- /client/src/components/MicTally.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import '../assets/css/MicTally.css' 5 | import { Store } from 'redux' 6 | import { IFader } from '../../../shared/src/reducers/fadersReducer' 7 | import { IChannels } from '../../../shared/src/reducers/channelsReducer' 8 | import { ISettings } from '../../../shared/src/reducers/settingsReducer' 9 | import { getFaderLabel } from '../utils/labels' 10 | 11 | import { 12 | SOCKET_TOGGLE_PGM, 13 | SOCKET_TOGGLE_VO, 14 | } from '../../../shared/src/constants/SOCKET_IO_DISPATCHERS' 15 | 16 | 17 | interface IMicTallyInjectProps { 18 | channels: IChannels 19 | faders: IFader[] 20 | settings: ISettings 21 | } 22 | 23 | interface IMicTallyState { 24 | toggleTargetIndex: number | null 25 | } 26 | 27 | class MicTally extends React.Component { 28 | constructor(props: any) { 29 | super(props) 30 | this.props.settings.showMonitorOptions = -1 31 | this.state = { 32 | toggleTargetIndex: null 33 | } 34 | } 35 | 36 | componentWillMount() { 37 | document.body.classList.add('v-mic-tally') 38 | } 39 | 40 | componentWillUnmount() { 41 | document.body.classList.remove('v-mic-tally') 42 | } 43 | 44 | preToggleFader(index: number) { 45 | const fader = this.props.faders[index] 46 | if (!fader || fader.muteOn) { 47 | return 48 | } 49 | this.setState({ toggleTargetIndex: index }) 50 | } 51 | clearToggleFaderIndex() { 52 | this.setState({ toggleTargetIndex: null }) 53 | } 54 | 55 | toggleFader(index: number) { 56 | const fader = this.props.faders[index] 57 | const faderLabel = getFaderLabel(index) 58 | 59 | if (fader.muteOn) { 60 | return 61 | } 62 | 63 | if (fader.voOn) { 64 | window.socketIoClient.emit(SOCKET_TOGGLE_VO, index) 65 | } else { 66 | window.socketIoClient.emit(SOCKET_TOGGLE_PGM, index) 67 | } 68 | this.clearToggleFaderIndex() 69 | } 70 | 71 | faderIsOn(index: number) { 72 | const fader = this.props.faders[index] 73 | return Boolean(fader && (fader.pgmOn || fader.voOn) && !fader.muteOn) 74 | } 75 | 76 | render() { 77 | const hasToggleTargetIndex = this.state.toggleTargetIndex !== null 78 | return ( 79 |
80 |
    81 | { this.props.faders 82 | .map((fader, index) => { 83 | if (!fader.showInMiniMonitor) { 84 | return 85 | } 86 | const isOn = this.faderIsOn(index) 87 | return ( 88 |
  • 89 |
    this.preToggleFader(index)} className={`c-mic-tally__status${isOn ? ' on': ''}${fader.muteOn ? ' muted' : ''}`}> 90 |
    91 | { isOn ? 'ON' : 'OFF' } 92 |
    93 |
    94 | {getFaderLabel(index)} 95 |
  • 96 | ) 97 | }) 98 | } 99 |
100 |
101 |
102 |

Are you sure you want to turn '{ getFaderLabel(this.state.toggleTargetIndex) }' {this.faderIsOn(this.state.toggleTargetIndex) ? 'OFF' : 'ON'}?

103 |
104 | 105 | 106 |
107 |
108 |
109 |
110 | ) 111 | } 112 | } 113 | 114 | const mapStateToProps = (state: any): IMicTallyInjectProps => { 115 | return { 116 | channels: state.channels[0].chMixerConnection[0].channel, 117 | faders: state.faders[0].fader, 118 | settings: state.settings[0], 119 | } 120 | } 121 | 122 | export default connect(mapStateToProps)( 123 | MicTally 124 | ) 125 | -------------------------------------------------------------------------------- /client/src/components/MiniChanStrip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactSlider from 'react-slider' 3 | 4 | import '../assets/css/MiniChanStrip.css' 5 | import { Store } from 'redux' 6 | import { connect } from 'react-redux' 7 | import { IFader } from '../../../shared/src/reducers/fadersReducer' 8 | import { SOCKET_SET_AUX_LEVEL } from '../../../shared/src/constants/SOCKET_IO_DISPATCHERS' 9 | import { getFaderLabel } from '../utils/labels' 10 | 11 | interface IChanStripInjectProps { 12 | label: string 13 | selectedProtocol: string 14 | numberOfChannelsInType: Array 15 | channel: Array 16 | fader: Array 17 | auxSendIndex: number 18 | offtubeMode: boolean 19 | } 20 | 21 | interface IChanStripProps { 22 | faderIndex: number 23 | } 24 | 25 | class MiniChanStrip extends React.PureComponent< 26 | IChanStripProps & IChanStripInjectProps & Store 27 | > { 28 | constructor(props: any) { 29 | super(props) 30 | } 31 | 32 | handleMonitorLevel(event: any, channelIndex: number) { 33 | window.socketIoClient.emit(SOCKET_SET_AUX_LEVEL, { 34 | channel: channelIndex, 35 | auxIndex: this.props.auxSendIndex, 36 | level: parseFloat(event), 37 | }) 38 | } 39 | 40 | monitor(channelIndex: number) { 41 | let faderIndex = this.props.channel[channelIndex].assignedFader 42 | if (faderIndex === -1) return null 43 | let monitorName = getFaderLabel(faderIndex, 'Fader') 44 | return ( 45 |
  • 46 | {monitorName} 47 | { 61 | this.handleMonitorLevel(event, channelIndex) 62 | }} 63 | /> 64 |

    _______

    65 |
  • 66 | ) 67 | } 68 | parameters() { 69 | return ( 70 |
    71 |
    72 | {this.props.label || 'FADER ' + (this.props.faderIndex + 1)} 73 | {' - MONITOR MIX MINUS'} 74 |
    75 |
      76 | {this.props.channel.map((ch: any, index: number) => { 77 | if (ch.auxLevel[this.props.auxSendIndex] >= 0) { 78 | return this.monitor(index) 79 | } 80 | })} 81 |
    82 |
    83 | ) 84 | } 85 | 86 | render() { 87 | if (this.props.faderIndex >= 0) { 88 | return ( 89 |
    90 | {this.props.offtubeMode ? this.parameters() : null} 91 |
    92 | ) 93 | } else { 94 | return
    95 | } 96 | } 97 | } 98 | 99 | const mapStateToProps = (state: any, props: any): IChanStripInjectProps => { 100 | let inject: IChanStripInjectProps = { 101 | label: '', 102 | selectedProtocol: state.settings[0].mixers[0].mixerProtocol, 103 | numberOfChannelsInType: 104 | state.settings[0].mixers[0].numberOfChannelsInType, 105 | channel: state.channels[0].chMixerConnection[0].channel, 106 | fader: state.faders[0].fader, 107 | auxSendIndex: -1, 108 | offtubeMode: state.settings[0].offtubeMode, 109 | } 110 | if (props.faderIndex >= 0) { 111 | inject = { 112 | label: getFaderLabel(props.faderIndex), 113 | selectedProtocol: state.settings[0].mixers[0].mixerProtocol, 114 | numberOfChannelsInType: 115 | state.settings[0].mixers[0].numberOfChannelsInType, 116 | channel: state.channels[0].chMixerConnection[0].channel, 117 | fader: state.faders[0].fader, 118 | auxSendIndex: state.faders[0].fader[props.faderIndex].monitor - 1, 119 | offtubeMode: state.settings[0].offtubeMode, 120 | } 121 | } 122 | return inject 123 | } 124 | 125 | export default connect(mapStateToProps)( 126 | MiniChanStrip 127 | ) as any 128 | -------------------------------------------------------------------------------- /client/src/components/MiniChannel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ClassNames from 'classnames' 3 | import { connect } from 'react-redux' 4 | import { Store } from 'redux' 5 | import '../assets/css/NoUiSlider.css' 6 | 7 | //assets: 8 | import '../assets/css/MiniChannel.css' 9 | import { IFader } from '../../../shared/src/reducers/fadersReducer' 10 | import { IChannels } from '../../../shared/src/reducers/channelsReducer' 11 | import { ISettings } from '../../../shared/src/reducers/settingsReducer' 12 | import { storeShowChanStrip } from '../../../shared/src/actions/settingsActions' 13 | import { getFaderLabel } from '../utils/labels' 14 | 15 | interface IChannelInjectProps { 16 | channels: IChannels 17 | fader: IFader 18 | settings: ISettings 19 | channelType: number 20 | channelTypeIndex: number 21 | label: string 22 | } 23 | 24 | interface IChannelProps { 25 | faderIndex: number 26 | } 27 | 28 | class MiniChannel extends React.Component< 29 | IChannelProps & IChannelInjectProps & Store 30 | > { 31 | faderIndex: number 32 | 33 | constructor(props: any) { 34 | super(props) 35 | this.faderIndex = this.props.faderIndex 36 | } 37 | 38 | public shouldComponentUpdate(nextProps: IChannelInjectProps) { 39 | return ( 40 | nextProps.fader.showChannel != this.props.fader.showChannel || 41 | nextProps.label != this.props.label || 42 | nextProps.settings.showChanStrip != 43 | this.props.settings.showChanStrip 44 | ) 45 | } 46 | 47 | handleShowChanStrip() { 48 | this.props.dispatch(storeShowChanStrip(this.faderIndex)) 49 | } 50 | 51 | chanStripButton = () => { 52 | return ( 53 | 65 | ) 66 | } 67 | 68 | render() { 69 | return this.props.fader.showChannel === false ? null : ( 70 |
    71 | 72 | {this.chanStripButton()} 73 |
    74 |
    75 |
    76 | ) 77 | } 78 | } 79 | 80 | const mapStateToProps = (state: any, props: any): IChannelInjectProps => { 81 | return { 82 | channels: state.channels[0].chMixerConnection[0].channel, 83 | fader: state.faders[0].fader[props.faderIndex], 84 | settings: state.settings[0], 85 | channelType: 0 /* TODO: state.channels[0].channel[props.channelIndex].channelType, */, 86 | channelTypeIndex: 87 | props.faderIndex /* TODO: state.channels[0].chMixerConnection[0].channel[props.channelIndex].channelTypeIndex, */, 88 | label: getFaderLabel(props.faderIndex) 89 | } 90 | } 91 | 92 | export default connect(mapStateToProps)( 93 | MiniChannel 94 | ) as any 95 | -------------------------------------------------------------------------------- /client/src/components/MiniChannels.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import '../assets/css/MiniChannels.css' 5 | import { Store } from 'redux' 6 | import { IFader } from '../../../shared/src/reducers/fadersReducer' 7 | import { IChannels } from '../../../shared/src/reducers/channelsReducer' 8 | import { ISettings } from '../../../shared/src/reducers/settingsReducer' 9 | import MiniChannel from './MiniChannel' 10 | import MiniChanStrip from './MiniChanStrip' 11 | 12 | interface IChannelsInjectProps { 13 | channels: IChannels 14 | faders: IFader[] 15 | settings: ISettings 16 | } 17 | 18 | class Channels extends React.Component { 19 | constructor(props: any) { 20 | super(props) 21 | this.props.settings.showMonitorOptions = -1 22 | } 23 | 24 | public shouldComponentUpdate(nextProps: IChannelsInjectProps) { 25 | return ( 26 | this.props.settings.showOptions !== 27 | nextProps.settings.showOptions || 28 | this.props.settings.showChanStrip !== 29 | nextProps.settings.showChanStrip || 30 | this.props.settings.showMonitorOptions !== 31 | nextProps.settings.showMonitorOptions || 32 | this.props.settings.mixers[0].mixerOnline !== 33 | nextProps.settings.mixers[0].mixerOnline || 34 | this.props.faders.length !== nextProps.faders.length 35 | ) 36 | } 37 | 38 | render() { 39 | return ( 40 |
    41 | {this.props.faders.map((fader: IFader, index: number) => { 42 | return fader.showInMiniMonitor ? ( 43 | 44 | ) : null 45 | })} 46 | {this.props.settings.showChanStrip >= 0 ? ( 47 |
    48 | 51 |
    52 | ) : ( 53 |
    54 | 57 |
    58 | )} 59 |
    60 | ) 61 | } 62 | } 63 | 64 | const mapStateToProps = (state: any): IChannelsInjectProps => { 65 | return { 66 | channels: state.channels[0].chMixerConnection[0].channel, 67 | faders: state.faders[0].fader, 68 | settings: state.settings[0], 69 | } 70 | } 71 | 72 | export default connect(mapStateToProps)( 73 | Channels 74 | ) 75 | -------------------------------------------------------------------------------- /client/src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import LanguageDetector from 'i18next-browser-languagedetector' 3 | 4 | i18n.use(LanguageDetector).init({ 5 | resources: { 6 | en: { 7 | translations: { 8 | 'TRYING TO CONNECT TO SISYFOS SERVER': 9 | 'TRYING TO CONNECT TO SISYFOS SERVER', 10 | VO: 'VO', 11 | PFL: 'PFL', 12 | 'CUE NEXT': 'CUE NEXT', 13 | PST: 'PST', 14 | 'SLOW FADE': 'SLOW FADE', 15 | }, 16 | }, 17 | nn: { 18 | translations: { 19 | VO: 'STK', 20 | }, 21 | }, 22 | nb: { 23 | translations: { 24 | VO: 'STK', 25 | }, 26 | }, 27 | sv: { 28 | translations: { 29 | VO: 'VO', 30 | }, 31 | }, 32 | }, 33 | whitelist: ['en', 'nn', 'nb', 'sv'], 34 | fallbackLng: 'en', 35 | debug: true, 36 | 37 | // have a common namespace used around the full app 38 | ns: ['translations'], 39 | defaultNS: 'translations', 40 | 41 | keySeparator: false, // we use content as keys 42 | 43 | interpolation: { 44 | escapeValue: false, // not needed for react!! 45 | formatSeparator: ',', 46 | }, 47 | 48 | react: { 49 | wait: true, 50 | }, 51 | }) 52 | 53 | export default i18n 54 | -------------------------------------------------------------------------------- /client/src/utils/labels.ts: -------------------------------------------------------------------------------- 1 | import { IStore } from '../../../shared/src/reducers/store' 2 | 3 | export function getChannelLabel( 4 | state: IStore, 5 | faderIndex: number 6 | ): string | undefined { 7 | return state.channels[0].chMixerConnection 8 | .flatMap((conn) => 9 | conn.channel.map((ch) => ({ 10 | assignedFader: ch.assignedFader, 11 | label: ch.label, 12 | })) 13 | ) 14 | .filter((ch) => ch.label && ch.label !== '') 15 | .find((ch) => ch.assignedFader === faderIndex)?.label 16 | } 17 | 18 | export function getFaderLabel(faderIndex: number, defaultName = 'CH'): string { 19 | const state: IStore = window.reduxState 20 | const automationLabel = 21 | state.faders[0].fader[faderIndex] && 22 | state.faders[0].fader[faderIndex].label !== '' 23 | ? state.faders[0].fader[faderIndex].label 24 | : undefined 25 | const userLabel = 26 | state.faders[0].fader[faderIndex] && 27 | state.faders[0].fader[faderIndex].userLabel !== '' 28 | ? state.faders[0].fader[faderIndex].userLabel 29 | : undefined 30 | const channelLabel = getChannelLabel(state, faderIndex) 31 | 32 | switch (state.settings[0].labelType) { 33 | case 'automation': 34 | return automationLabel || defaultName + ' ' + (faderIndex + 1) 35 | case 'user': 36 | return userLabel || defaultName + ' ' + (faderIndex + 1) 37 | case 'channel': 38 | return channelLabel || defaultName + ' ' + (faderIndex + 1) 39 | case 'automatic': 40 | default: 41 | return ( 42 | userLabel || 43 | automationLabel || 44 | channelLabel || 45 | defaultName + ' ' + (faderIndex + 1) 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | // Search under node_modules for non-relative imports. 5 | "moduleResolution": "node", 6 | // Process & infer types from .js files. 7 | "allowJs": false, 8 | // Don't emit; allow Babel to transform files. 9 | "noEmit": false, 10 | // Enable strictest settings like strictNullChecks & noImplicitAny. 11 | "strict": false, 12 | // Disallow features that require cross-file information for emit. 13 | "isolatedModules": false, 14 | // Import non-ES modules as default imports. 15 | "esModuleInterop": true, 16 | //Support for jsx. 17 | "jsx": "react", 18 | // Check for "any" while converting to TS this can be turned off. 19 | "noImplicitAny": true, 20 | "module": "commonjs", 21 | "resolveJsonModule": true, 22 | "allowSyntheticDefaultImports": true, 23 | "outDir": "../dist" 24 | }, 25 | "include": ["../client", "../shared"], 26 | "exclude": ["../**/node_modules/*", "../**/dist", "../**/*.spec.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | 5 | // Any directories you will be adding code/files into, need to be added to this array so webpack will pick them up 6 | const defaultInclude = [ 7 | path.resolve(__dirname, '../client'), 8 | path.resolve(__dirname, '../shared'), 9 | ] 10 | 11 | module.exports = { 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.css$/, 16 | use: ['style-loader', 'css-loader'], 17 | }, 18 | { 19 | test: /\.tsx?$/, 20 | loader: 'ts-loader', 21 | options: { 22 | configFile: 'tsconfig.json', 23 | }, 24 | }, 25 | { 26 | test: /\.(jpe?g|png|gif)$/, 27 | use: [ 28 | { 29 | loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]', 30 | }, 31 | ], 32 | include: defaultInclude, 33 | }, 34 | { 35 | test: /\.(eot|svg|ttf|woff|woff2)$/, 36 | use: [ 37 | { 38 | loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]', 39 | }, 40 | ], 41 | include: defaultInclude, 42 | }, 43 | ], 44 | }, 45 | entry: '.', 46 | resolve: { 47 | extensions: ['.tsx', '.ts', '.js'], 48 | }, 49 | target: 'web', 50 | output: { 51 | path: path.resolve(__dirname, 'dist'), 52 | }, 53 | plugins: [ 54 | new HtmlWebpackPlugin({ 55 | template: './index.ejs', 56 | inject: true, 57 | }), 58 | new webpack.DefinePlugin({ 59 | 'process.env.NODE_ENV': JSON.stringify('production'), 60 | }), 61 | ], 62 | stats: { 63 | colors: true, 64 | children: false, 65 | chunks: false, 66 | modules: false, 67 | }, 68 | } 69 | -------------------------------------------------------------------------------- /desktop/electron.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const { app, BrowserWindow } = require('electron') 4 | const server = require('server/dist/server') 5 | 6 | function createWindow() { 7 | // Create the browser window. 8 | const win = new BrowserWindow({ 9 | width: 1920, 10 | height: 1080, 11 | minWidth: 1000, 12 | minHeight: 800, 13 | darkTheme: true, 14 | webPreferences: { 15 | nodeIntegration: true, 16 | }, 17 | }) 18 | 19 | win.loadURL('http://localhost:1176/?settings=1') 20 | } 21 | 22 | // This method will be called when Electron has finished 23 | // initialization and is ready to create browser windows. 24 | // Some APIs can only be used after this event occurs. 25 | app.whenReady().then(createWindow) 26 | app.on('window-all-closed', app.quit) 27 | 28 | app.on('activate', () => { 29 | if (BrowserWindow.getAllWindows().length === 0) { 30 | createWindow() 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /desktop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "desktop", 3 | "version": "4.17.0", 4 | "main": "electron.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "yarn build:windows", 8 | "build:macos": "yarn prepare && electron-builder -m", 9 | "build:windows": "yarn prepare && electron-builder -w", 10 | "prepare": "node prepare-package.js" 11 | }, 12 | "dependencies": { 13 | "server": "^1.0.38" 14 | }, 15 | "devDependencies": { 16 | "electron": "16.2.6", 17 | "electron-builder": "22.14.5" 18 | }, 19 | "build": { 20 | "appId": "com.sisyfos-audio-controller.app", 21 | "asar": true, 22 | "directories": { 23 | "output": "dist" 24 | }, 25 | "productName": "Sisyfos Audio Controller" 26 | }, 27 | "description": "Audio mixer build with the logic from a video mixer", 28 | "author": { 29 | "name": "Kasper Olsson Hans (TV2 Denmark)", 30 | "email": "github@olzzon.dk" 31 | }, 32 | "contributors": [ 33 | { 34 | "name": "Balte de Wit", 35 | "email": "balte@superfly.tv", 36 | "url": "https://superfly.tv" 37 | }, 38 | { 39 | "name": "Jan Starzak", 40 | "email": "jan@superfly.tv", 41 | "url": "https://superfly.tv" 42 | }, 43 | { 44 | "name": "Anders Frederik Jørgensen", 45 | "email": "afjo@tv2.dk" 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /desktop/prepare-package.js: -------------------------------------------------------------------------------- 1 | const rootPackage = require('../package.json') 2 | const electronPackage = require('./package.json') 3 | const fs = require('fs') 4 | 5 | const newPackage = { ...electronPackage } 6 | newPackage.version = rootPackage.version 7 | newPackage.description = rootPackage.description 8 | newPackage.license = rootPackage.license 9 | newPackage.author = rootPackage.author 10 | newPackage.contributors = rootPackage.contributors 11 | newPackage.build.productName = rootPackage.name 12 | .split('-') 13 | .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`) 14 | .join(' ') 15 | 16 | try { 17 | if (JSON.stringify(newPackage) !== JSON.stringify(electronPackage)) { 18 | fs.writeFileSync( 19 | './package.json', 20 | JSON.stringify(newPackage, null, 4), 21 | 'utf-8' 22 | ) 23 | } 24 | } catch (error) { 25 | console.error(error) 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sisyfos-audio-controller", 3 | "version": "4.19.0", 4 | "description": "Audio mixer build with the logic from a video mixer", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Kasper Olsson Hans (TV2 Denmark)", 8 | "email": "github@olzzon.dk" 9 | }, 10 | "contributors": [ 11 | { 12 | "name": "Balte de Wit", 13 | "email": "balte@superfly.tv", 14 | "url": "https://superfly.tv" 15 | }, 16 | { 17 | "name": "Jan Starzak", 18 | "email": "jan@superfly.tv", 19 | "url": "https://superfly.tv" 20 | }, 21 | { 22 | "name": "Anders Frederik Jørgensen", 23 | "email": "afjo@tv2.dk" 24 | } 25 | ], 26 | "keywords": [ 27 | "app", 28 | "audio", 29 | "open-source" 30 | ], 31 | "engines": { 32 | "node": ">=18.15.0", 33 | "yarn": ">=1.22.0" 34 | }, 35 | "private": true, 36 | "workspaces": [ 37 | "client", 38 | "server", 39 | "shared", 40 | "desktop" 41 | ], 42 | "scripts": { 43 | "start": "cross-env NODE_ENV=production node server/dist/server", 44 | "start:dev": "cross-env NODE_ENV=development node --inspect server/dist/server", 45 | "start:local": "cross-env NODE_ENV=local node --inspect server/dist/server", 46 | "build": "yarn build:client && yarn build:server", 47 | "build:client": "yarn --cwd ./client build", 48 | "build:server": "yarn --cwd ./server build", 49 | "build:desktop": "yarn --cwd ./desktop build", 50 | "watch": "concurrently --kill-others \"yarn watch:server\" \"yarn watch:client\"", 51 | "watch:client": "yarn --cwd ./client watch", 52 | "watch:server": "yarn --cwd ./server watch", 53 | "test": "yarn test:client && yarn test:server", 54 | "test:client": "yarn --cwd ./client test", 55 | "test:server": "yarn --cwd ./server test", 56 | "test:watch": "concurrently --kill-others \"yarn --cwd ./client test:watch\" \"yarn --cwd ./server test:watch\"", 57 | "lint": "prettier --write", 58 | "validate": "yarn validate:dependencies && yarn validate:license", 59 | "validate:dependencies": "yarn audit --groups dependencies", 60 | "validate:license": "node-license-validator -p -d --allow-licenses MIT MIT/X11 BSD 0BSD BSD-3-Clause BSD-2-Clause ISC Apache Apache-2.0 WTFPL CC-BY-3.0 CC-BY-4.0 CC0-1.0 GPL Python-2.0 Unlicense --allow-packages cycle", 61 | "clean": "rimraf **/dist **/release", 62 | "reset": "yarn clean && rimraf **/node_modules node_modules" 63 | }, 64 | "simple-git-hooks": { 65 | "pre-commit": "yarn lint-staged" 66 | }, 67 | "lint-staged": { 68 | "*.{js,ts,css,json,md}": [ 69 | "prettier --write" 70 | ] 71 | }, 72 | "dependencies": { 73 | "@reduxjs/toolkit": "^2.1.0", 74 | "cross-env": "^7.0.3" 75 | }, 76 | "devDependencies": { 77 | "concurrently": "^8.0.1", 78 | "lint-staged": "^13.2.2", 79 | "node-license-validator": "^1.3.2", 80 | "prettier": "^2.8.8", 81 | "rimraf": "^5.0.0" 82 | }, 83 | "resolutions": { 84 | "xml2js": "^0.5.0", 85 | "socket.io-parser": "^4.2.3" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /server/@types/classnames/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'classnames' { 2 | function ClassNames (...args: (string | { [key: string]: any })[]): string 3 | export = ClassNames 4 | } -------------------------------------------------------------------------------- /server/@types/emberplus/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'emberplus'; 2 | -------------------------------------------------------------------------------- /server/@types/osc/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'osc' { 2 | import { EventEmitter } from 'events' 3 | import * as dgram from 'dgram' 4 | 5 | interface TimeTag { 6 | raw: number[], 7 | native: number 8 | } 9 | 10 | interface OSCMessage { 11 | address: string, 12 | args: any[] 13 | } 14 | 15 | type OSCMessageOrBundle = OSCMessage | OSCBundle 16 | 17 | interface OSCBundle { 18 | timeTag: TimeTag, 19 | packets: OSCMessageOrBundle[] 20 | } 21 | 22 | type OSCData = any 23 | 24 | interface Colour { 25 | r: number, 26 | g: number, 27 | b: number, 28 | a: number 29 | } 30 | 31 | interface OSCPort extends EventEmitter { 32 | on(event: 'ready', handler: () => void): this 33 | on(event: 'message', handler: (message: OSCMessage, timeTag: TimeTag | undefined, info: any) => void): this 34 | on(event: 'bundle', handler: (bundle: OSCBundle, timeTag: TimeTag, info: any) => void): this 35 | on(event: 'osc', handler: (packet: OSCMessageOrBundle, info: any) => void): this 36 | on(event: 'raw', handler: (data: Uint8Array, info: any) => void): this 37 | on(event: 'error', handler: (error: Error) => void): this 38 | send(packet: OSCMessage | OSCBundle): void 39 | } 40 | 41 | interface UDPPortOptions { 42 | localPort: number, 43 | localAddress: string, 44 | remotePort?: number, 45 | remoteAddress?: string, 46 | broadcast?: boolean, 47 | multicastTTL?: number, 48 | multicastMembership?: string[], 49 | socket?: dgram.Socket, 50 | metadata?: boolean 51 | } 52 | class UDPPort extends EventEmitter implements OSCPort { 53 | constructor(options: UDPPortOptions) 54 | on(event: "ready", handler: () => void): this 55 | on(event: "message", handler: (message: OSCMessage, timeTag: TimeTag | undefined, info: any) => void): this 56 | on(event: "bundle", handler: (bundle: OSCBundle, timeTag: TimeTag, info: any) => void): this 57 | on(event: "osc", handler: (packet: OSCMessage | OSCBundle, info: any) => void): this 58 | on(event: "raw", handler: (data: Uint8Array, info: any) => void): this 59 | on(event: "error", handler: (error: Error) => void): this 60 | send(packet: OSCMessage | OSCBundle): void 61 | open(): void 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /server/@types/web-midi-api/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'web-midi-api' { 2 | // To ensure that it can be imported. 3 | } 4 | -------------------------------------------------------------------------------- /server/__tests__/__mocks__/parsedEmptyStore.json: -------------------------------------------------------------------------------- 1 | { 2 | "faders": [ 3 | { 4 | "vuMeters": [], 5 | "fader": [] 6 | } 7 | ], 8 | "channels": [ 9 | { 10 | "chMixerConnection": [ 11 | { 12 | "channel": [ 13 | { 14 | "channelType": 0, 15 | "channelTypeIndex": 0, 16 | "assignedFader": 0, 17 | "fadeActive": false, 18 | "outputLevel": 0, 19 | "auxLevel": [] 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | ], 26 | "settings": [ 27 | { 28 | "showSettings": false, 29 | "showChanStrip": -1, 30 | "showOptions": false, 31 | "showMonitorOptions": -1, 32 | "showStorage": false, 33 | "numberOfMixers": 1, 34 | "mixers": [ 35 | { 36 | "mixerProtocol": "sslSystemT", 37 | "deviceIp": "0.0.0.0", 38 | "devicePort": 10024, 39 | "protocolLatency": 220, 40 | "mixerMidiInputPort": "", 41 | "mixerMidiOutputPort": "", 42 | "numberOfChannelsInType": [8], 43 | "numberOfAux": 0, 44 | "nextSendAux": -1, 45 | "mixerOnline": false, 46 | "localIp": "0.0.0.0", 47 | "localOscPort": 1234 48 | } 49 | ], 50 | "enableRemoteFader": false, 51 | "remoteFaderMidiInputPort": "", 52 | "remoteFaderMidiOutputPort": "", 53 | "numberOfFaders": 8, 54 | "voLevel": 30, 55 | "autoResetLevel": 5, 56 | "automationMode": true, 57 | "offtubeMode": false, 58 | "fadeTime": 120, 59 | "voFadeTime": 280, 60 | "showPfl": false, 61 | "serverOnline": true, 62 | "currentPage": "undefined", 63 | "customPages": "undefined" 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /server/__tests__/__mocks__/parsedSimpleStore.json: -------------------------------------------------------------------------------- 1 | { 2 | "faders": [ 3 | { 4 | "vuMeters": [ 5 | { 6 | "vuVal": 0, 7 | "reductionVal": 0 8 | } 9 | ], 10 | "fader": [ 11 | { 12 | "faderLevel": 0.75, 13 | "label": "", 14 | "pgmOn": false, 15 | "voOn": false, 16 | "pstOn": false, 17 | "pstVoOn": false, 18 | "pflOn": false, 19 | "muteOn": false, 20 | "fxParam": [ 21 | { "key": 0, "value": null }, 22 | { "key": 1, "value": null }, 23 | { "key": 2, "value": null }, 24 | { "key": 3, "value": null } 25 | ], 26 | "threshold": 0.75, 27 | "ratio": 0.75, 28 | "monitor": 1, 29 | "showChannel": true 30 | } 31 | ] 32 | } 33 | ], 34 | "channels": [ 35 | { 36 | "chMixerConnection": [ 37 | { 38 | "channel": [ 39 | { 40 | "channelType": 0, 41 | "channelTypeIndex": 0, 42 | "assignedFader": 0, 43 | "fadeActive": false, 44 | "outputLevel": 0, 45 | "auxLevel": [] 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | ], 52 | "settings": [ 53 | { 54 | "showSettings": false, 55 | "showChanStrip": -1, 56 | "showOptions": false, 57 | "showMonitorOptions": -1, 58 | "showStorage": false, 59 | "mixerProtocol": "genericMidi", 60 | "numberOfMixers": 1, 61 | "mixers": [ 62 | { 63 | "mixerProtocol": "sslSystemT", 64 | "deviceIp": "0.0.0.0", 65 | "devicePort": 10024, 66 | "protocolLatency": 220, 67 | "mixerMidiInputPort": "", 68 | "mixerMidiOutputPort": "", 69 | "numberOfChannelsInType": [8], 70 | "numberOfAux": 0, 71 | "nextSendAux": -1, 72 | "mixerOnline": false, 73 | "localIp": "0.0.0.0", 74 | "localOscPort": 1234 75 | } 76 | ], 77 | "enableRemoteFader": false, 78 | "remoteFaderMidiInputPort": "", 79 | "remoteFaderMidiOutputPort": "", 80 | "numberOfFaders": 8, 81 | "voLevel": 20, 82 | "autoResetLevel": 10, 83 | "automationMode": true, 84 | "offtubeMode": false, 85 | "fadeTime": 60, 86 | "voFadeTime": 200, 87 | "showPfl": false, 88 | "serverOnline": true, 89 | "currentPage": "undefined", 90 | "customPages": "undefined" 91 | } 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /server/__tests__/channelReducer.spec.ts: -------------------------------------------------------------------------------- 1 | import indexReducer from '../../shared/src/reducers/indexReducer' 2 | import { 3 | storeFadeActive, 4 | storeSetAssignedFader, 5 | storeSetCompleteChState, 6 | storeSetOutputLevel, 7 | } from '../../shared/src/actions/channelActions' 8 | import { 9 | IChannel, 10 | InumberOfChannels, 11 | } from '../../shared/src/reducers/channelsReducer' 12 | 13 | import fs from 'fs' 14 | const parsedFullStoreJSON = fs.readFileSync( 15 | '__tests__/__mocks__/parsedFullStore.json', 16 | 'utf-8' 17 | ) 18 | 19 | describe('Test redux channelReducer actions', () => { 20 | /** 21 | * TEST SET_OUTPUT_LEVEL: 22 | */ 23 | 24 | it('should return the new output_level state on channels', () => { 25 | let parsedFullStore = JSON.parse(parsedFullStoreJSON) 26 | let nextState = JSON.parse(parsedFullStoreJSON) 27 | nextState.channels[0].chMixerConnection[0].channel[10].outputLevel = 0.5 28 | expect( 29 | indexReducer(parsedFullStore, storeSetOutputLevel(0, 10, 0.5)) 30 | ).toEqual(nextState) 31 | }) 32 | 33 | /** 34 | * TEST SET_ASSIGNED_FADER: 35 | */ 36 | 37 | it('should return the new assignedFader state on channels', () => { 38 | let parsedFullStore = JSON.parse(parsedFullStoreJSON) 39 | let nextState = JSON.parse(parsedFullStoreJSON) 40 | nextState.channels[0].chMixerConnection[0].channel[10].assignedFader = 2 41 | expect( 42 | indexReducer(parsedFullStore, storeSetAssignedFader(0, 10, 2)) 43 | ).toEqual(nextState) 44 | }) 45 | 46 | /** 47 | * TEST FADE_ACTIVE: 48 | */ 49 | 50 | it('should return the new FADE_ACTIVE state on channels', () => { 51 | let parsedFullStore = JSON.parse(parsedFullStoreJSON) 52 | let nextState = JSON.parse(parsedFullStoreJSON) 53 | nextState.channels[0].chMixerConnection[0].channel[10].fadeActive = true 54 | expect( 55 | indexReducer(parsedFullStore, storeFadeActive(0, 10, true)) 56 | ).toEqual(nextState) 57 | }) 58 | 59 | /** 60 | * TEST SET_COMPLETE_CHANNEL_STATE: 61 | */ 62 | 63 | it('should return the new COMPLETE_CHANNEL_STATE on channels', () => { 64 | let parsedFullStore = JSON.parse(parsedFullStoreJSON) 65 | let nextState = JSON.parse(parsedFullStoreJSON) 66 | let channels: IChannel[] = [] 67 | let numberOfChannels: InumberOfChannels[] = [{ numberOfTypeInCh: [24] }] 68 | 69 | for (let i = 0; i < 24; i++) { 70 | channels.push({ 71 | channelType: 0, 72 | channelTypeIndex: i, 73 | assignedFader: i, 74 | auxLevel: [], 75 | fadeActive: false, 76 | outputLevel: 0.75, 77 | }) 78 | nextState.channels[0].chMixerConnection[0].channel[ 79 | i 80 | ].outputLevel = 0.75 81 | } 82 | expect( 83 | indexReducer( 84 | parsedFullStore, 85 | storeSetCompleteChState( 86 | { chMixerConnection: [{ channel: channels }] }, 87 | numberOfChannels 88 | ) 89 | ) 90 | ).toEqual(nextState) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /server/__tests__/indexReducer.spec.ts: -------------------------------------------------------------------------------- 1 | import indexReducer from '../../shared/src/reducers/indexReducer' 2 | 3 | import fs from 'fs' 4 | const parsedEmptyStoreJSON = fs.readFileSync( 5 | '__tests__/__mocks__/parsedEmptyStore.json', 6 | 'utf-8' 7 | ) 8 | 9 | describe('Test initialize store', () => { 10 | let parsedInitialStore = JSON.parse(parsedEmptyStoreJSON) 11 | it('should return the initial state of the whole Store', () => { 12 | // ** Uncomment to update initial settings state: 13 | // let data = indexReducer(undefined, {type: ''}) 14 | // fs.writeFileSync('__tests__/__mocks__/parsedEmptyStore-UPDATE.json', JSON.stringify(data)) 15 | 16 | expect( 17 | indexReducer(JSON.parse(parsedEmptyStoreJSON), { type: '' }) 18 | ).toEqual(parsedInitialStore) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /server/__tests__/settingsReducer.spec.ts: -------------------------------------------------------------------------------- 1 | import indexReducer from '../../shared/src/reducers/indexReducer' 2 | import { 3 | TOGGLE_SHOW_CHAN_STRIP, 4 | TOGGLE_SHOW_OPTION, 5 | TOGGLE_SHOW_SETTINGS, 6 | TOGGLE_SHOW_STORAGE, 7 | UPDATE_SETTINGS, 8 | } from '../../shared/src/actions/settingsActions' 9 | 10 | import fs from 'fs' 11 | const parsedFullStoreJSON = fs.readFileSync( 12 | '__tests__/__mocks__/parsedFullStore.json', 13 | 'utf-8' 14 | ) 15 | 16 | describe('Test redux settingsReducer actions', () => { 17 | /** 18 | * TEST TOGGLE_SHOW_CHAN_STRIP: 19 | */ 20 | 21 | it('should return the new showChanStrip state on settings', () => { 22 | let parsedFullStore = JSON.parse(parsedFullStoreJSON) 23 | let nextState = JSON.parse(parsedFullStoreJSON) 24 | nextState.settings[0].showChanStrip = 3 25 | expect( 26 | indexReducer(parsedFullStore, { 27 | type: TOGGLE_SHOW_CHAN_STRIP, 28 | channel: 3, 29 | }) 30 | ).toEqual(nextState) 31 | 32 | parsedFullStore = JSON.parse(parsedFullStoreJSON) 33 | expect( 34 | indexReducer(nextState, { 35 | type: TOGGLE_SHOW_CHAN_STRIP, 36 | channel: -1, 37 | }) 38 | ).toEqual(parsedFullStore) 39 | }) 40 | 41 | /** 42 | * TEST TOGGLE_SHOW_OPTION: 43 | */ 44 | 45 | it('should return the new showOptions state on settings', () => { 46 | let parsedFullStore = JSON.parse(parsedFullStoreJSON) 47 | let nextState = JSON.parse(parsedFullStoreJSON) 48 | nextState.settings[0].showOptions = 3 49 | expect( 50 | indexReducer(parsedFullStore, { 51 | type: TOGGLE_SHOW_OPTION, 52 | channel: 3, 53 | }) 54 | ).toEqual(nextState) 55 | 56 | parsedFullStore = JSON.parse(parsedFullStoreJSON) 57 | expect( 58 | indexReducer(nextState, { 59 | type: TOGGLE_SHOW_OPTION, 60 | channel: false, 61 | }) 62 | ).toEqual(parsedFullStore) 63 | }) 64 | 65 | /** 66 | * TEST TOGGLE_SHOW_SETTINGS: 67 | */ 68 | 69 | it('should return the new showSettings state on settings', () => { 70 | let parsedFullStore = JSON.parse(parsedFullStoreJSON) 71 | let nextState = JSON.parse(parsedFullStoreJSON) 72 | nextState.settings[0].showSettings = true 73 | expect( 74 | indexReducer(parsedFullStore, { 75 | type: TOGGLE_SHOW_SETTINGS, 76 | }) 77 | ).toEqual(nextState) 78 | }) 79 | 80 | /** 81 | * TEST TOGGLE_SHOW_STORAGE: 82 | */ 83 | 84 | it('should return the new showStorage state on settings', () => { 85 | let parsedFullStore = JSON.parse(parsedFullStoreJSON) 86 | let nextState = JSON.parse(parsedFullStoreJSON) 87 | nextState.settings[0].showStorage = true 88 | expect( 89 | indexReducer(parsedFullStore, { 90 | type: TOGGLE_SHOW_STORAGE, 91 | }) 92 | ).toEqual(nextState) 93 | }) 94 | 95 | /** 96 | * TEST UPDATE_SETTINGS: 97 | */ 98 | 99 | it('should return the new settings state on settings', () => { 100 | let parsedFullStore = JSON.parse(parsedFullStoreJSON) 101 | let nextState = JSON.parse(parsedFullStoreJSON) 102 | expect( 103 | indexReducer(parsedFullStore, { 104 | type: UPDATE_SETTINGS, 105 | settings: parsedFullStore.settings[0], 106 | }) 107 | ).toEqual(nextState) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import { MainThreadHandlers } from './src/MainThreadHandler' 2 | import { expressInit } from './src/expressHandler' 3 | 4 | declare global { 5 | namespace NodeJS { 6 | interface Global { 7 | mainThreadHandler: MainThreadHandlers 8 | navigator: any // Workaround for WebMidi 9 | performance: any // Workaround for WebMidi 10 | } 11 | } 12 | } 13 | 14 | global.mainThreadHandler = new MainThreadHandlers() 15 | expressInit() 16 | -------------------------------------------------------------------------------- /server/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "tsc", 7 | "watch": "tsc --watch", 8 | "test": "jest", 9 | "test:watch": "jest --watch" 10 | }, 11 | "devDependencies": { 12 | "@types/express": "^4.17.17", 13 | "@types/jest": "^29.5.1", 14 | "@types/node": "^18.16.0", 15 | "@types/webmidi": "^2.0.7", 16 | "jest": "^29.5.0", 17 | "ts-jest": "^29.1.0", 18 | "typescript": "^5.0.4" 19 | }, 20 | "dependencies": { 21 | "@babel/core": "^7.22.10", 22 | "@tv2media/logger": "^2.0.1", 23 | "atem-connection": "^3.2.0", 24 | "casparcg-connection": "^5.1.0", 25 | "emberplus-connection": "^0.1.2", 26 | "express": "^4.18.2", 27 | "node-emberplus": "^3.0.5", 28 | "node-vmix": "^1.6.1", 29 | "osc": "^2.4.4", 30 | "performance-now": "^2.1.0", 31 | "redux": "^4.2.1", 32 | "socket.io": "^4.6.2", 33 | "tslib": "^2.5.0", 34 | "vmix-js-utils": "^4.0.16", 35 | "web-midi-api": "^2.2.5", 36 | "webmidi": "^2.5.1", 37 | "shared": "file:../shared" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/src/expressHandler.ts: -------------------------------------------------------------------------------- 1 | import { logger } from './utils/logger' 2 | import { socketSubscribeVu, socketUnsubscribeVu } from './utils/vuServer' 3 | 4 | import express from 'express' 5 | import path from 'path' 6 | import { Server } from 'http' 7 | import { Server as SocketServer } from 'socket.io' 8 | const app = express() 9 | const server = new Server(app) 10 | const socketServer = new SocketServer(server) 11 | const SERVER_PORT = 1176 12 | const staticPath = path.join( 13 | path.dirname(require.resolve('client/package.json')), 14 | 'dist' 15 | ) 16 | logger.data(staticPath).debug('Express static file path:') 17 | app.use('/', express.static(staticPath)) 18 | server.listen(SERVER_PORT) 19 | logger.info(`Server started at http://localhost:${SERVER_PORT}`) 20 | 21 | socketServer.on('connection', (socket: any) => { 22 | logger.info(`Client connected: ${socket.client.id}`) 23 | global.mainThreadHandler.socketServerHandlers(socket) 24 | 25 | socket.on('subscribe-vu-meter', () => { 26 | logger.debug('Socket subscribe vu') 27 | socketSubscribeVu(socket) 28 | }) 29 | socket.on('disconnect', () => { 30 | socketUnsubscribeVu(socket) 31 | }) 32 | }) 33 | 34 | export const expressInit = () => { 35 | logger.info('Initialising WebServer') 36 | } 37 | 38 | export { socketServer } 39 | -------------------------------------------------------------------------------- /server/src/mainClasses.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MixerProtocolPresets, 3 | MixerProtocolList, 4 | } from '../../shared/src/constants/MixerProtocolPresets' 5 | import { MixerGenericConnection } from './utils/MixerConnection' 6 | import { AutomationConnection } from './utils/AutomationConnection' 7 | import { RemoteConnection } from './utils/RemoteConnection' 8 | 9 | const mixerProtocolPresets = MixerProtocolPresets 10 | const mixerProtocolList = MixerProtocolList 11 | 12 | const mixerGenericConnection = new MixerGenericConnection() 13 | const automationConnection = new AutomationConnection() 14 | const remoteConnections = new RemoteConnection() 15 | 16 | export { 17 | mixerProtocolList, 18 | mixerProtocolPresets, 19 | mixerGenericConnection, 20 | automationConnection, 21 | remoteConnections, 22 | } 23 | -------------------------------------------------------------------------------- /server/src/reducers/store.ts: -------------------------------------------------------------------------------- 1 | import storeRedux from '../../../shared/src/reducers/store' 2 | import { storeUpdateSettings } from '../../../shared/src/actions/settingsActions' 3 | import { loadSettings } from '../utils/SettingsStorage' 4 | 5 | storeRedux.dispatch(storeUpdateSettings(loadSettings(storeRedux.getState()))) 6 | 7 | //Subscribe to redux store: 8 | let state = storeRedux.getState() 9 | const unsubscribe = storeRedux.subscribe(() => { 10 | state = storeRedux.getState() 11 | }) 12 | 13 | export { storeRedux as store } 14 | export { state } -------------------------------------------------------------------------------- /server/src/utils/RemoteConnection.ts: -------------------------------------------------------------------------------- 1 | import { HuiMidiRemoteConnection } from './remoteConnections/HuiMidiRemoteConnection' 2 | import { SkaarhojRemoteConnection } from './remoteConnections/SkaarhojRemoteConnection' 3 | 4 | export class RemoteConnection { 5 | store: any 6 | remoteConnection: any 7 | 8 | constructor() { 9 | // HUI SUPPORT IS DISABLED AND SKAARHOJ IS ALWAYS ON 10 | // HUI needs to be updated so for now it´s always disabled. 11 | this.remoteConnection = new SkaarhojRemoteConnection() 12 | /* 13 | if (state.settings[0].enableRemoteFader) { 14 | this.remoteConnection = new HuiMidiRemoteConnection() 15 | } 16 | */ 17 | } 18 | 19 | updateRemoteFaderState(channelIndex: number, outputLevel: number) { 20 | this.remoteConnection.updateRemoteFaderState(channelIndex, outputLevel) 21 | } 22 | 23 | updateRemoteAuxPanels() { 24 | this.remoteConnection.updateRemoteAuxPanels() 25 | } 26 | 27 | updateRemotePgmPstPfl(channelIndex: number) { 28 | this.remoteConnection.updateRemotePgmPstPfl(channelIndex) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/utils/SnapshotHandler.ts: -------------------------------------------------------------------------------- 1 | //Utils: 2 | import { loadSnapshotState, saveSnapshotState } from './SettingsStorage' 3 | import { mixerProtocolPresets } from '../mainClasses' 4 | import { state } from '../reducers/store' 5 | import { logger } from './logger' 6 | import { InumberOfChannels } from '../../../shared/src/reducers/channelsReducer' 7 | import { STORAGE_FOLDER } from './SettingsStorage' 8 | 9 | import path from 'path' 10 | export class SnapshotHandler { 11 | numberOfChannels: InumberOfChannels[] = [] 12 | 13 | constructor() { 14 | logger.info('Setting up state') 15 | 16 | this.snapShopStoreTimer() 17 | 18 | // Count total number of channels: 19 | for ( 20 | let mixerIndex = 0; 21 | mixerIndex < state.settings[0].numberOfMixers; 22 | mixerIndex++ 23 | ) { 24 | this.numberOfChannels.push({ numberOfTypeInCh: [] }) 25 | mixerProtocolPresets[ 26 | state.settings[0].mixers[mixerIndex].mixerProtocol 27 | ].channelTypes.forEach((item: any, index: number) => { 28 | this.numberOfChannels[mixerIndex].numberOfTypeInCh.push( 29 | state.settings[0].mixers[mixerIndex].numberOfChannelsInType[ 30 | index 31 | ] 32 | ) 33 | }) 34 | } 35 | this.loadSnapshotSettings( 36 | path.resolve(STORAGE_FOLDER, 'default.shot'), 37 | true 38 | ) 39 | 40 | // ** UNCOMMENT TO DUMP A FULL STORE: 41 | //const fs = require('fs') 42 | //fs.writeFileSync('src/components/__tests__/__mocks__/parsedFullStore-UPDATE.json', JSON.stringify(global.storeRedux.getState())) 43 | } 44 | 45 | snapShopStoreTimer() { 46 | const saveTimer = setInterval(() => { 47 | let snapshot = { 48 | faderState: state.faders[0], 49 | channelState: state.channels[0], 50 | } 51 | saveSnapshotState( 52 | snapshot, 53 | path.resolve(STORAGE_FOLDER, 'default.shot') 54 | ) 55 | }, 2000) 56 | } 57 | 58 | loadSnapshotSettings(fileName: string, loadAll: boolean) { 59 | loadSnapshotState( 60 | state.faders[0], 61 | state.channels[0], 62 | this.numberOfChannels, 63 | state.settings[0].numberOfFaders, 64 | fileName, 65 | loadAll 66 | ) 67 | } 68 | 69 | saveSnapshotSettings(fileName: string) { 70 | let snapshot = { 71 | faderState: state.faders[0], 72 | channelState: state.channels[0], 73 | } 74 | saveSnapshotState(snapshot, fileName) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /server/src/utils/labels.ts: -------------------------------------------------------------------------------- 1 | import { IStore } from '../../../shared/src/reducers/store' 2 | import { state } from '../reducers/store' 3 | 4 | export function getChannelLabel( 5 | state: IStore, 6 | faderIndex: number 7 | ): string | undefined { 8 | return state.channels[0].chMixerConnection 9 | .map((conn) => 10 | conn.channel.map((ch) => ({ 11 | assignedFader: ch.assignedFader, 12 | label: ch.label, 13 | })) 14 | ) 15 | .reduce((a, b) => [...a, ...b], []) // flatten 16 | .filter((ch) => ch.label && ch.label !== '') 17 | .find((ch) => ch.assignedFader === faderIndex)?.label 18 | } 19 | 20 | export function getFaderLabel(faderIndex: number, defaultName = 'CH'): string { 21 | const automationLabel = 22 | state.faders[0].fader[faderIndex] && 23 | state.faders[0].fader[faderIndex].label !== '' 24 | ? state.faders[0].fader[faderIndex].label 25 | : undefined 26 | const userLabel = 27 | state.faders[0].fader[faderIndex] && 28 | state.faders[0].fader[faderIndex].userLabel !== '' 29 | ? state.faders[0].fader[faderIndex].userLabel 30 | : undefined 31 | const channelLabel = getChannelLabel(state, faderIndex) 32 | 33 | switch (state.settings[0].labelType) { 34 | case 'automation': 35 | return automationLabel || defaultName + ' ' + (faderIndex + 1) 36 | case 'user': 37 | return userLabel || defaultName + ' ' + (faderIndex + 1) 38 | case 'channel': 39 | return channelLabel || defaultName + ' ' + (faderIndex + 1) 40 | case 'automatic': 41 | default: 42 | return ( 43 | userLabel || 44 | automationLabel || 45 | channelLabel || 46 | defaultName + ' ' + (faderIndex + 1) 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { DefaultLogger } from '@tv2media/logger/node' 2 | 3 | export const logger = new DefaultLogger() 4 | -------------------------------------------------------------------------------- /server/src/utils/migrations.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import { logger } from './logger' 5 | import { ISettings } from '../../../shared/src/reducers/settingsReducer' 6 | import { getSnapShotList, IShotStorage } from './SettingsStorage' 7 | 8 | const version = process.env.npm_package_version 9 | const settingsPath: string = path.resolve(process.cwd(), 'storage') 10 | 11 | export const checkVersion = (currentSettings: ISettings): ISettings => { 12 | if ( 13 | versionAsNumber(version) > 14 | versionAsNumber(currentSettings.sisyfosVersion) 15 | ) { 16 | currentSettings = migrateVersion(currentSettings) 17 | } 18 | return currentSettings 19 | } 20 | 21 | const migrateVersion = (currentSettings: ISettings): ISettings => { 22 | logger.info( 23 | `Migrating VERSION from ${currentSettings.sisyfosVersion} to ${version}` 24 | ) 25 | let newSettings = currentSettings 26 | if (versionAsNumber(version) >= versionAsNumber('4.7.0')) { 27 | newSettings = migrate45to47(currentSettings) 28 | } 29 | return newSettings 30 | } 31 | 32 | const migrate45to47 = (currentSettings: ISettings): ISettings => { 33 | let files: string[] = getSnapShotList() 34 | files.push('default.shot') 35 | 36 | files.forEach((fileName: string) => { 37 | let stateFromShot = JSON.parse( 38 | fs.readFileSync(path.join(settingsPath, fileName), 'utf8') 39 | ) 40 | // As this is the first implemented migration it also looks .shot files from ealier versions than 4.xx 41 | if (stateFromShot.channelState.chConnection) { 42 | // From Version 4.xx 43 | stateFromShot.channelState.chMixerConnection = 44 | stateFromShot.channelState?.chConnection 45 | delete stateFromShot.channelState.chConnection 46 | } else if (stateFromShot.channelState.channel) { 47 | // Version lower than 4.0 48 | stateFromShot.channelState.chMixerConnection = [ 49 | { channel: stateFromShot.channelState.channel }, 50 | ] 51 | delete stateFromShot.channelState.channel 52 | } 53 | let migratedShot: IShotStorage = stateFromShot 54 | try { 55 | fs.writeFileSync( 56 | path.join(settingsPath, fileName), 57 | JSON.stringify(migratedShot), 58 | 'utf8' 59 | ) 60 | logger.trace(`Snapshot ${fileName} Saved to storage folder`) 61 | } catch (error: any) { 62 | logger.data(error).error('Error saving Snapshot') 63 | } 64 | }) 65 | currentSettings.sisyfosVersion = version 66 | delete currentSettings.customPages 67 | try { 68 | fs.writeFileSync( 69 | path.join(settingsPath, 'settings.json'), 70 | JSON.stringify(currentSettings), 71 | 'utf8' 72 | ) 73 | } catch (error: any) { 74 | logger 75 | .data(error) 76 | .error('Migration failed - error writing settings.json file') 77 | } 78 | return currentSettings 79 | } 80 | 81 | const versionAsNumber = (versionString: string): number => { 82 | if (!versionString) return 0 83 | let versionArray: string[] = versionString.split('.') 84 | let versionValue: number = 85 | parseInt(versionArray[0]) * 10000 + 86 | parseInt(versionArray[1]) * 100 + 87 | (parseInt(versionArray[2]) || 0) 88 | return versionValue 89 | } 90 | -------------------------------------------------------------------------------- /server/src/utils/mixerConnections/productSpecific/behringerXr.ts: -------------------------------------------------------------------------------- 1 | import { state } from '../../../reducers/store' 2 | import { sendVuLevel } from '../../vuServer' 3 | import { VuType } from '../../../../../shared/src/utils/vu-server-types' 4 | 5 | const DATA_OFFSET = 4 6 | 7 | export const behringerXrMeter = (mixerIndex: number, message: any) => { 8 | //Test data from Behringer: 9 | //message = [40, 0, 0, 0, 133, 157, 183, 156, 72, 154, 101, 157, 229, 162, 241, 158, 253, 162, 156, 162, 131, 162, 253, 162, 81, 162, 29, 162, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 0, 128, 223, 157, 223, 157, 223, 157, 223, 157]; 10 | 11 | let uint8bytes = Uint8Array.from(message[0]) 12 | let dataview = new DataView(uint8bytes.buffer) 13 | 14 | for ( 15 | let i = 0; 16 | i < state.settings[0].mixers[0].numberOfChannelsInType[0]; 17 | i++ 18 | ) { 19 | let level = (dataview.getInt16(DATA_OFFSET + 2 * i, true) + 8000) / 8000 20 | sendVuLevel(i, VuType.Channel, 0, level) 21 | } 22 | } 23 | 24 | export const behringerReductionMeter = (mixerIndex: number, message: any) => { 25 | //Test data from Behringer: 26 | //message = 27 | 28 | let uint8bytes = Uint8Array.from(message[0]) 29 | let dataview = new DataView(uint8bytes.buffer) 30 | 31 | for ( 32 | let i = 0; 33 | i < state.settings[0].mixers[0].numberOfChannelsInType[0]; 34 | i++ 35 | ) { 36 | let level = 37 | 1 - 38 | (dataview.getInt16(DATA_OFFSET + 2 * (i + 16), true) + 8000) / 8000 39 | sendVuLevel(i, VuType.Reduction, 0, level) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/src/utils/mixerConnections/productSpecific/midas.ts: -------------------------------------------------------------------------------- 1 | import { state } from '../../../reducers/store' 2 | import { sendVuLevel } from '../../vuServer' 3 | import { VuType } from '../../../../../shared/src/utils/vu-server-types' 4 | 5 | const calcVuLevel = (level: number) => { 6 | return Math.log(level) / Math.log(600) + 1 7 | } 8 | 9 | export const midasMeter = (mixerIndex: number, message: any) => { 10 | const DATA_OFFSET = 4 11 | let uint8bytes = Uint8Array.from(message[0]) 12 | let dataview = new DataView(uint8bytes.buffer) 13 | let level: number 14 | let reductionLevel: number 15 | let assignedFader: number 16 | let numberOfChannels = 17 | state.settings[0].mixers[mixerIndex].numberOfChannelsInType[0] 18 | 19 | for (let i = 0; i < numberOfChannels; i++) { 20 | assignedFader = 21 | state.channels[0].chMixerConnection[mixerIndex].channel[i] 22 | .assignedFader 23 | if ( 24 | assignedFader >= 0 && 25 | assignedFader < state.settings[0].numberOfFaders 26 | ) { 27 | level = calcVuLevel(dataview.getFloat32(4 * i + DATA_OFFSET, true)) 28 | reductionLevel = dataview.getFloat32( 29 | 4 * (i + 64) + DATA_OFFSET, 30 | true 31 | ) 32 | let vuIndex: number = state.faders[0].fader[ 33 | assignedFader 34 | ].assignedChannels?.findIndex((assigned) => { 35 | return ( 36 | assigned.mixerIndex === mixerIndex && 37 | assigned.channelIndex === i 38 | ) 39 | }) 40 | if (vuIndex === -1) vuIndex = 0 41 | sendVuLevel(assignedFader, VuType.Channel, vuIndex, level) 42 | sendVuLevel(assignedFader, VuType.Reduction, 0, 1 - reductionLevel) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/src/utils/vuServer.ts: -------------------------------------------------------------------------------- 1 | import { VuType } from '../../../shared/src/utils/vu-server-types' 2 | 3 | const sockets: Array = [] 4 | 5 | export function socketSubscribeVu(socket: any) { 6 | const i = sockets.indexOf(socket) 7 | if (i === -1) { 8 | sockets.push(socket) 9 | } 10 | } 11 | 12 | export function socketUnsubscribeVu(socket: any) { 13 | const i = sockets.indexOf(socket) 14 | if (i >= 0) { 15 | sockets.splice(i, 1) 16 | } 17 | } 18 | 19 | export function sendVuLevel( 20 | faderIndex: number, 21 | type: VuType, 22 | channelIndex: number, 23 | level: number 24 | ) { 25 | sockets.forEach((socket) => { 26 | socket.emit(type, faderIndex, channelIndex, level) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["esnext", "dom"], 5 | "moduleResolution": "node", 6 | "allowJs": false, 7 | "strict": false, 8 | "isolatedModules": false, 9 | "esModuleInterop": true, 10 | "noImplicitAny": false, 11 | "resolveJsonModule": true, 12 | "module": "commonjs", 13 | "outDir": "dist" 14 | }, 15 | "include": ["../server", "../../shared"], 16 | "exclude": ["node_modules/*", "dist/*", "**/*.spec.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "devDependencies": { 6 | "@types/node": "^18.16.0", 7 | "typescript": "^5.0.4" 8 | }, 9 | "dependencies": { 10 | "redux": "^4.2.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /shared/src/actions/channelActions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IChannel, 3 | IChannels, 4 | InumberOfChannels, 5 | } from '../reducers/channelsReducer' 6 | 7 | export enum ChannelActionTypes { 8 | SET_OUTPUT_LEVEL = 'SET_OUTPUT_LEVEL', 9 | SET_AUX_LEVEL = 'SET_AUX_LEVEL', 10 | SET_COMPLETE_CH_STATE = 'SET_COMPLETE_CH_STATE', 11 | SET_SINGLE_CH_STATE = 'SET_SINGLE_CH_STATE', 12 | FADE_ACTIVE = 'FADE_ACTIVE', 13 | SET_ASSIGNED_FADER = 'SET_ASSIGNED_FADER', 14 | SET_PRIVATE = 'SET_PRIVATE', 15 | SET_CHANNEL_LABEL = 'SET_CHANNEL_LABEL', 16 | FLUSH_CHANNEL_LABELS = 'FLUSH_CHANNEL_LABELS', 17 | } 18 | 19 | export type ChannelActions = 20 | | { 21 | type: ChannelActionTypes.SET_OUTPUT_LEVEL 22 | mixerIndex: number 23 | channel: number 24 | level: number 25 | } 26 | | { 27 | type: ChannelActionTypes.SET_COMPLETE_CH_STATE 28 | numberOfTypeChannels: InumberOfChannels[] 29 | allState: IChannels 30 | } 31 | | { 32 | type: ChannelActionTypes.SET_SINGLE_CH_STATE 33 | channelIndex: number 34 | state: IChannel 35 | } 36 | | { 37 | type: ChannelActionTypes.FADE_ACTIVE 38 | mixerIndex: number 39 | channel: number 40 | active: boolean 41 | } 42 | | { 43 | type: ChannelActionTypes.SET_ASSIGNED_FADER 44 | mixerIndex: number 45 | channel: number 46 | faderNumber: number 47 | } 48 | | { 49 | type: ChannelActionTypes.SET_AUX_LEVEL 50 | mixerIndex: number 51 | channel: number 52 | auxIndex: number 53 | level: number 54 | } 55 | | { 56 | type: ChannelActionTypes.SET_PRIVATE 57 | mixerIndex: number 58 | channel: number 59 | tag: string 60 | value: string 61 | } 62 | | { 63 | type: ChannelActionTypes.SET_CHANNEL_LABEL 64 | mixerIndex: number 65 | channel: number 66 | label: string 67 | } 68 | | { type: ChannelActionTypes.FLUSH_CHANNEL_LABELS } 69 | -------------------------------------------------------------------------------- /shared/src/actions/settingsActions.ts: -------------------------------------------------------------------------------- 1 | import { ICustomPages, PageType } from '../reducers/settingsReducer' 2 | 3 | export const TOGGLE_SHOW_SETTINGS = 'TOGGLE_SHOW_SETTINGS' 4 | export const TOGGLE_SHOW_PAGES_SETUP = 'TOGGLE_SHOW_PAGES_SETUP' 5 | export const TOGGLE_SHOW_LABEL_SETTINGS = 'TOGGLE_SHOW_LABEL_SETTINGS' 6 | export const TOGGLE_SHOW_CHAN_STRIP = 'TOGGLE_SHOW_CHAN_STRIP' 7 | export const TOGGLE_SHOW_CHAN_STRIP_FULL = 'TOGGLE_SHOW_CHAN_STRIP_FULL' 8 | export const TOGGLE_SHOW_OPTION = 'TOGGLE_SHOW_OPTION' 9 | export const TOGGLE_SHOW_MONITOR_OPTIONS = 'TOGGLE_SHOW_MONITOR_OPTIONS' 10 | export const TOGGLE_SHOW_STORAGE = 'TOGGLE_SHOW_STORAGE' 11 | export const UPDATE_SETTINGS = 'UPDATE_SETTINGS' 12 | export const SET_MIXER_ONLINE = 'SET_MIXER_ONLINE' 13 | export const SET_SERVER_ONLINE = 'SET_SERVER_ONLINE' 14 | export const SET_PAGE = 'SET_PAGE' 15 | export const SET_PAGES_LIST = 'SET_PAGES_LIST' 16 | 17 | export const storeShowSettings = () => { 18 | return { 19 | type: TOGGLE_SHOW_SETTINGS, 20 | } 21 | } 22 | export const storeShowPagesSetup = () => { 23 | return { 24 | type: TOGGLE_SHOW_PAGES_SETUP, 25 | } 26 | } 27 | export const storeShowLabelSetup = () => { 28 | return { 29 | type: TOGGLE_SHOW_LABEL_SETTINGS, 30 | } 31 | } 32 | export const storeShowChanStrip = (faderIndex: number) => { 33 | return { 34 | type: TOGGLE_SHOW_CHAN_STRIP, 35 | channel: faderIndex, 36 | } 37 | } 38 | export const storeShowChanStripFull = (faderIndex: number) => { 39 | return { 40 | type: TOGGLE_SHOW_CHAN_STRIP_FULL, 41 | channel: faderIndex, 42 | } 43 | } 44 | export const storeShowOptions = (faderIndex: number) => { 45 | return { 46 | type: TOGGLE_SHOW_OPTION, 47 | channel: faderIndex, 48 | } 49 | } 50 | export const storeShowMonitorOptions = (faderIndex: number) => { 51 | return { 52 | type: TOGGLE_SHOW_MONITOR_OPTIONS, 53 | channel: faderIndex, 54 | } 55 | } 56 | export const storeShowStorage = () => { 57 | return { 58 | type: TOGGLE_SHOW_STORAGE, 59 | } 60 | } 61 | export const storeUpdateSettings = (settings: any) => { 62 | return { 63 | type: UPDATE_SETTINGS, 64 | settings: settings, 65 | } 66 | } 67 | export const storeUpdatePagesList = (customPages: ICustomPages[]) => { 68 | return { 69 | type: SET_PAGES_LIST, 70 | customPages: customPages, 71 | } 72 | } 73 | export const storeSetMixerOnline = ( 74 | mixerIndex: number, 75 | mixerOnline: boolean 76 | ) => { 77 | return { 78 | type: SET_MIXER_ONLINE, 79 | mixerIndex: mixerIndex, 80 | mixerOnline: mixerOnline, 81 | } 82 | } 83 | export const storeSetServerOnline = (serverOnline: boolean) => { 84 | return { 85 | type: SET_SERVER_ONLINE, 86 | serverOnline: serverOnline, 87 | } 88 | } 89 | export const storeSetPage = (pageType: PageType, id: number | string) => { 90 | if (typeof id === 'string') { 91 | return { 92 | type: SET_PAGE, 93 | pageType: pageType, 94 | id: id, 95 | start: 0, 96 | } 97 | } else { 98 | return { 99 | type: SET_PAGE, 100 | pageType: pageType, 101 | id: '', 102 | start: id, 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /shared/src/actions/utils/dbConversion.ts: -------------------------------------------------------------------------------- 1 | import { VuLabelConversionType } from '../../constants/MixerProtocolInterface' 2 | 3 | export const Conversions = { 4 | [VuLabelConversionType.Decibel]: { 5 | to: (f: number): number => { 6 | if (f >= 0.5) { 7 | return f * 40 - 30 // max dB value: +10. 8 | } else if (f >= 0.25) { 9 | return f * 80 - 50 10 | } else if (f >= 0.0625) { 11 | return f * 160 - 70 12 | } else if (f > 0.0) { 13 | return f * 480 - 90 // min dB value: -90 or -oo 14 | } else { 15 | return -Infinity 16 | } 17 | }, 18 | from: (d: number): number => { 19 | let f: number 20 | if (d < -60) { 21 | f = (d + 90) / 480 22 | } else if (d < -30) { 23 | f = (d + 70) / 160 24 | } else if (d < -10) { 25 | f = (d + 50) / 80 26 | } else if (d <= 10) { 27 | f = (d + 30) / 40 28 | } else { 29 | f = 1 30 | } 31 | return Math.max(0, f) 32 | }, 33 | }, 34 | [VuLabelConversionType.DecibelRuby]: { 35 | to: (f: number): number => 36 | Math.max(Conversions[VuLabelConversionType.Decibel].to(f), -191), 37 | from: (d: number): number => 38 | Conversions[VuLabelConversionType.Decibel].from(d), 39 | }, 40 | [VuLabelConversionType.DecibelMC2]: { 41 | to: (f: number): number => 42 | Math.max(Conversions[VuLabelConversionType.Decibel].to(f), -128), 43 | from: (d: number): number => 44 | Conversions[VuLabelConversionType.Decibel].from(d), 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /shared/src/constants/AutomationPresets.ts: -------------------------------------------------------------------------------- 1 | //While developing mixer specific settings will be in one file. 2 | //At first release these will be in seperate files 3 | //So it´s easy to add new equipment. 4 | 5 | export interface IAutomationProtocol { 6 | protocol: string 7 | label: string 8 | mode: string 9 | leadingZeros: boolean 10 | initializeCommands: [ 11 | { 12 | mixerMessage: string 13 | value: string 14 | type: string 15 | } 16 | ] 17 | fromAutomation: { 18 | CHANNEL_PGM_ON_OFF: string 19 | CHANNEL_PST_ON_OFF: string 20 | CHANNEL_FADER_LEVEL: string 21 | INJECT_COMMAND: string 22 | CHANNEL_VISIBLE: string 23 | CHANNEL_MUTE: string 24 | X_MIX: string 25 | SET_LABEL: string 26 | FADE_TO_BLACK: string 27 | CLEAR_PST: string 28 | SNAP_RECALL: string 29 | STATE_CHANNEL_PGM: string 30 | STATE_CHANNEL_PST: string 31 | STATE_CHANNEL_FADER_LEVEL: string 32 | STATE_CHANNEL_MUTE: string 33 | STATE_FULL: string 34 | PING: string 35 | } 36 | toAutomation: { 37 | STATE_CHANNEL_PGM: string 38 | STATE_CHANNEL_PST: string 39 | STATE_CHANNEL_FADER_LEVEL: string 40 | STATE_CHANNEL_MUTE: string 41 | STATE_FULL: string 42 | PONG: string 43 | } 44 | fader: { 45 | min: number 46 | max: number 47 | zero: number 48 | step: number 49 | } 50 | meter: { 51 | min: number 52 | max: number 53 | zero: number 54 | test: number 55 | } 56 | } 57 | 58 | export const AutomationPresets: { [key: string]: IAutomationProtocol } = { 59 | sofie: { 60 | protocol: 'OSC', 61 | label: 'Sofie Automation', 62 | mode: 'client', 63 | leadingZeros: true, 64 | initializeCommands: [ 65 | { 66 | mixerMessage: '/info', 67 | value: '', 68 | type: 'f', 69 | }, 70 | ], 71 | fromAutomation: { 72 | CHANNEL_PGM_ON_OFF: '/ch/{value1}/pgm', 73 | CHANNEL_PST_ON_OFF: '/ch/{value1}/pst', 74 | CHANNEL_FADER_LEVEL: '/ch/{value1}/faderlevel', 75 | CHANNEL_VISIBLE: '/ch/{value1}/visible', 76 | CHANNEL_MUTE: '/ch/{value1}/mute', 77 | X_MIX: '/take', 78 | INJECT_COMMAND: '/inject', 79 | SET_LABEL: '/ch/{value1}/label', 80 | FADE_TO_BLACK: '/fadetoblack', 81 | CLEAR_PST: '/clearpst', 82 | SNAP_RECALL: '/snap/{value1}', 83 | STATE_CHANNEL_PGM: '/state/ch/{value1}/pgm', 84 | STATE_CHANNEL_PST: '/state/ch/{value1}/pst', 85 | STATE_CHANNEL_FADER_LEVEL: '/state/ch/{value1}/faderlevel', 86 | STATE_CHANNEL_MUTE: '/state/ch/{value1}/mute', 87 | STATE_FULL: '/state/full', 88 | PING: '/ping/{value1}', 89 | }, 90 | toAutomation: { 91 | STATE_CHANNEL_PGM: '/state/ch/{value1}/pgm', 92 | STATE_CHANNEL_PST: '/state/ch/{value1}/pst', 93 | STATE_CHANNEL_FADER_LEVEL: '/state/ch/{value1}/faderlevel', 94 | STATE_CHANNEL_MUTE: '/state/ch/{value1}/mute', 95 | STATE_FULL: '/state/full', 96 | PONG: '/pong', 97 | }, 98 | fader: { 99 | min: 0, 100 | max: 1, 101 | zero: 0.75, 102 | step: 0.01, 103 | }, 104 | meter: { 105 | min: 0, 106 | max: 1, 107 | zero: 0.75, 108 | test: 0.6, 109 | }, 110 | }, 111 | } 112 | 113 | export const AutomationProtocolList = Object.getOwnPropertyNames( 114 | AutomationPresets 115 | ).map((preset) => { 116 | return { 117 | value: preset, 118 | label: AutomationPresets[preset].label, 119 | } 120 | }) 121 | -------------------------------------------------------------------------------- /shared/src/constants/MixerProtocolInterface.ts: -------------------------------------------------------------------------------- 1 | export enum fxParamsList { 2 | EqGain01, 3 | EqGain02, 4 | EqGain03, 5 | EqGain04, 6 | EqFreq01, 7 | EqFreq02, 8 | EqFreq03, 9 | EqFreq04, 10 | EqQ01, 11 | EqQ02, 12 | EqQ03, 13 | EqQ04, 14 | DelayTime, 15 | GainTrim, 16 | CompThrs, 17 | CompRatio, 18 | CompKnee, 19 | CompMakeUp, 20 | CompAttack, 21 | CompHold, 22 | CompRelease, 23 | CompOnOff, 24 | } 25 | export enum VuLabelConversionType { 26 | Linear = 'linear', 27 | Decibel = 'decibel', 28 | DecibelRuby = 'decibelRuby', 29 | DecibelMC2 = 'decibelMC2', 30 | } 31 | export interface IMixerProtocolGeneric { 32 | protocol: string 33 | fxList?: {} 34 | label: string 35 | presetFileExtension?: string 36 | loadPresetCommand?: Array 37 | MAX_UPDATES_PER_SECOND: number 38 | vuLabelConversionType?: VuLabelConversionType 39 | vuLabelValues?: Array 40 | fader?: { 41 | min: number 42 | max: number 43 | zero: number 44 | step: number 45 | } 46 | meter?: { 47 | min: number 48 | max: number 49 | zero: number 50 | test: number 51 | } 52 | channelTypes: Array 53 | } 54 | 55 | export interface IMixerProtocol extends IMixerProtocolGeneric { 56 | leadingZeros?: boolean 57 | pingCommand?: Array // Simple command for pinging Audio mixer 58 | pingResponseCommand?: Array // Ping commands that expects responses 59 | pingTime?: number // How often should mixer ping the pingCommands 60 | mixerTimeout?: number // Max time between responses from AudioMixer 61 | initializeCommands?: Array 62 | } 63 | 64 | export interface IChannelTypes { 65 | channelTypeName: string 66 | channelTypeColor: string 67 | fromMixer: { 68 | CHANNEL_INPUT_GAIN?: Array 69 | CHANNEL_INPUT_SELECTOR?: Array 70 | CHANNEL_OUT_GAIN?: Array 71 | CHANNEL_VU?: Array 72 | CHANNEL_VU_REDUCTION?: Array 73 | CHANNEL_NAME?: Array 74 | PFL?: Array 75 | NEXT_SEND?: Array 76 | [FX_PARAM: number]: Array 77 | AUX_LEVEL?: Array 78 | CHANNEL_MUTE_ON?: Array 79 | CHANNEL_MUTE_OFF?: Array 80 | CHANNEL_AMIX?: Array 81 | } 82 | toMixer: { 83 | CHANNEL_INPUT_GAIN?: Array 84 | CHANNEL_INPUT_SELECTOR?: Array 85 | CHANNEL_OUT_GAIN?: Array 86 | CHANNEL_NAME?: Array 87 | PFL_ON?: Array 88 | PFL_OFF?: Array 89 | NEXT_SEND?: Array 90 | [FX_PARAM: number]: Array 91 | AUX_LEVEL?: Array 92 | CHANNEL_MUTE_ON?: Array 93 | CHANNEL_MUTE_OFF?: Array 94 | CHANNEL_AMIX?: Array 95 | } 96 | } 97 | 98 | interface IMixerMessageProtocol { 99 | mixerMessage: string 100 | value?: any 101 | type?: string 102 | min?: number 103 | max?: number 104 | zero?: number 105 | label?: string 106 | valueAsLabels?: Array 107 | valueLabel?: string 108 | minLabel?: number 109 | maxLabel?: number 110 | zeroLabel?: number 111 | } 112 | 113 | export interface IFxProtocol { 114 | key: fxParamsList 115 | params: Array 116 | } 117 | 118 | export const emptyMixerMessage = (): IMixerMessageProtocol => { 119 | return { 120 | mixerMessage: 'none', 121 | value: 0, 122 | type: '', 123 | min: 0, 124 | max: 0, 125 | zero: 0, 126 | } 127 | } 128 | 129 | // CasparCG Specific interfaces: 130 | export interface ICasparCGChannelLayerPair { 131 | channel: number 132 | layer: number 133 | } 134 | 135 | export interface ICasparCGMixerGeometryFile { 136 | label?: string 137 | channelLabels?: string[] 138 | fromMixer: { 139 | CHANNEL_VU: Array 140 | } 141 | toMixer: { 142 | PGM_CHANNEL_FADER_LEVEL: Array 143 | PFL_AUX_FADER_LEVEL: Array 144 | NEXT_AUX_FADER_LEVEL: Array 145 | CHANNEL_INPUT_SELECTOR?: Array 146 | } 147 | sourceOptions?: { 148 | sources: Array< 149 | ICasparCGChannelLayerPair & { 150 | producer: string 151 | file: string 152 | } 153 | > 154 | options: { 155 | [key: string]: { 156 | // producer property invocation 157 | [key: string]: string // label: property 158 | } 159 | } 160 | } 161 | } 162 | 163 | export interface ICasparCGMixerGeometry extends IMixerProtocolGeneric { 164 | studio: string 165 | leadingZeros: boolean 166 | pingTime: number 167 | fromMixer: { 168 | // CHANNEL_FADER_LEVEL: ChannelLayerPair[], 169 | // CHANNEL_OUT_GAIN: ChannelLayerPair[], 170 | CHANNEL_VU: Array 171 | } 172 | toMixer: { 173 | PGM_CHANNEL_FADER_LEVEL: Array 174 | PFL_AUX_FADER_LEVEL: Array 175 | NEXT_AUX_FADER_LEVEL: Array 176 | CHANNEL_INPUT_SELECTOR?: Array 177 | } 178 | channelLabels?: string[] 179 | sourceOptions?: { 180 | sources: Array< 181 | ICasparCGChannelLayerPair & { 182 | producer: string 183 | file: string 184 | } 185 | > 186 | options: { 187 | [key: string]: { 188 | // producer property invocation 189 | [key: string]: string // label: property 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /shared/src/constants/MixerProtocolPresets.ts: -------------------------------------------------------------------------------- 1 | import { ArdourMaster } from './mixerProtocols/ardourMaster' 2 | import { ReaperMaster } from './mixerProtocols/reaperMaster' 3 | import { BehringerXrMaster } from './mixerProtocols/behringerXrMaster' 4 | import { MidasMaster } from './mixerProtocols/midasMaster' 5 | import { GenericMidi } from './mixerProtocols/genericMidi' 6 | import { LawoRelayVrx4 } from './mixerProtocols/LawoRelayVrx4' 7 | import { LawoMC2 } from './mixerProtocols/LawoMC2' 8 | import { LawoRuby } from './mixerProtocols/LawoRuby' 9 | import { CasparCGMaster } from './mixerProtocols/casparCGMaster' 10 | import { DMXIS } from './mixerProtocols/DmxIs' 11 | import { YamahaQLCL } from './mixerProtocols/yamahaQLCL' 12 | import { SSLSystemT } from './mixerProtocols/SSLsystemT' 13 | import { StuderOnAirMaster } from './mixerProtocols/StuderOnAirEmber' 14 | import { StuderVistaMaster } from './mixerProtocols/StuderVistaEmber' 15 | import { VMix } from './mixerProtocols/vMix' 16 | import { Atem } from './mixerProtocols/atem' 17 | 18 | // Interface: 19 | import { IMixerProtocolGeneric } from './MixerProtocolInterface' 20 | 21 | export const MixerProtocolPresets: { 22 | [key: string]: IMixerProtocolGeneric 23 | } = Object.assign( 24 | { 25 | ardourMaster: ArdourMaster, 26 | reaperMaster: ReaperMaster, 27 | behringerxrmaster: BehringerXrMaster, 28 | midasMaster: MidasMaster, 29 | genericMidi: GenericMidi, 30 | lawoRelayVrx4: LawoRelayVrx4, 31 | lawoMC2: LawoMC2, 32 | lawoRuby: LawoRuby, 33 | dmxis: DMXIS, 34 | yamahaQlCl: YamahaQLCL, 35 | sslSystemT: SSLSystemT, 36 | studerOnAirMaster: StuderOnAirMaster, 37 | studerVistaMaster: StuderVistaMaster, 38 | vMix: VMix, 39 | atem: Atem, 40 | }, 41 | { 42 | casparCGMaster: CasparCGMaster, 43 | } 44 | ) 45 | /* 46 | */ 47 | 48 | export const MixerProtocolList = Object.getOwnPropertyNames( 49 | MixerProtocolPresets 50 | ).map((preset) => { 51 | return { 52 | value: preset, 53 | label: MixerProtocolPresets[preset].label, 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /shared/src/constants/SOCKET_IO_DISPATCHERS.ts: -------------------------------------------------------------------------------- 1 | // Dispatch constants: 2 | 3 | // Fader Channels: 4 | export const SOCKET_SET_FADERLEVEL = 'FaderLevel' 5 | export const SOCKET_SET_INPUT_GAIN = 'InputGain' 6 | export const SOCKET_SET_INPUT_SELECTOR = 'InputSelector' 7 | export const SOCKET_ASSIGN_CH_TO_FADER = 'assignChToFader' 8 | export const SOCKET_REMOVE_ALL_CH_ASSIGNMENTS = 'removeAllChAssignments' 9 | export const SOCKET_SET_FADER_MONITOR = 'FaderMonitor' 10 | export const SOCKET_SHOW_IN_MINI_MONITOR = 'showInMiniMonitor' 11 | export const SOCKET_SET_AUX_LEVEL = 'setAuxLevel' 12 | export const SOCKET_SET_INPUT_OPTION = 'setInputOption' 13 | export const SOCKET_SET_FX = 'setFx' 14 | export const SOCKET_TOGGLE_PGM = 'togglePgm' 15 | export const SOCKET_TOGGLE_VO = 'toggleVo' 16 | export const SOCKET_TOGGLE_SLOW_FADE = 'toggleSlowFade' 17 | export const SOCKET_TOGGLE_PST = 'togglePst' 18 | export const SOCKET_TOGGLE_PFL = 'togglePfl' 19 | export const SOCKET_TOGGLE_MUTE = 'toggleMute' 20 | export const SOCKET_TOGGLE_AMIX = 'toggleAmix' 21 | export const SOCKET_TOGGLE_IGNORE = 'toggleIgnore' 22 | export const SOCKET_NEXT_MIX = 'nextMix' 23 | export const SOCKET_CLEAR_PST = 'clearPst' 24 | 25 | // Div: 26 | export const SOCKET_SAVE_SETTINGS = 'saveSettings' 27 | export const SOCKET_RESTART_SERVER = 'restartServer' 28 | export const SOCKET_GET_SNAPSHOT_LIST = 'getSnapshotList' 29 | export const SOCKET_RETURN_SNAPSHOT_LIST = 'returnSnapshotList' 30 | export const SOCKET_GET_CCG_LIST = 'getCcgList' 31 | export const SOCKET_RETURN_CCG_LIST = 'returnCcgList' 32 | export const SOCKET_GET_MIXER_PRESET_LIST = 'getMixerPresetList' 33 | export const SOCKET_RETURN_MIXER_PRESET_LIST = 'returnMixerPresetList' 34 | export const SOCKET_LOAD_MIXER_PRESET = 'loadMixerPreset' 35 | export const SOCKET_LOAD_SNAPSHOT = 'loadSnapshot' 36 | export const SOCKET_SAVE_SNAPSHOT = 'saveSnapshot' 37 | export const SOCKET_SAVE_CCG_FILE = 'saveCcgFile' 38 | export const SOCKET_GET_PAGES_LIST = 'getPagesList' 39 | export const SOCKET_RETURN_PAGES_LIST = 'getPagesList' 40 | export const SOCKET_TOGGLE_ALL_MANUAL = 'toggleAllManual' 41 | export const SOCKET_GET_LABELS = 'getLabels' 42 | 43 | // Store updates: 44 | export const SOCKET_SET_FULL_STORE = 'setFullStore' 45 | export const SOCKET_SET_STORE_FADER = 'setStoreFader' 46 | export const SOCKET_SET_STORE_CHANNEL = 'setStoreChannel' 47 | export const SOCKET_SET_MIXER_ONLINE = 'setStoreSettings' 48 | export const SOCKET_SET_PAGES_LIST = 'setPagesList' 49 | export const SOCKET_SET_LABELS = 'setLabels' 50 | export const SOCKET_FLUSH_LABELS = 'flushLabels' 51 | -------------------------------------------------------------------------------- /shared/src/constants/mixerProtocols/DmxIs.ts: -------------------------------------------------------------------------------- 1 | import { IMixerProtocol } from '../MixerProtocolInterface' 2 | 3 | export const DMXIS: IMixerProtocol = { 4 | protocol: 'OSC', 5 | label: 'DMXIS Light Controller Protocol', 6 | presetFileExtension: '', 7 | MAX_UPDATES_PER_SECOND: 10, 8 | leadingZeros: false, //some OSC protocols needs channels to be 01, 02 etc. 9 | pingTime: 0, //Bypass ping when pingTime is zero 10 | channelTypes: [ 11 | { 12 | channelTypeName: 'CH', 13 | channelTypeColor: '#3f2f2f', 14 | fromMixer: { 15 | CHANNEL_OUT_GAIN: [ 16 | { 17 | mixerMessage: '/dmxis/ch/{channel}', 18 | value: 0, 19 | type: 'f', 20 | min: 0, 21 | max: 1, 22 | zero: 0.75, 23 | }, 24 | ], 25 | }, 26 | toMixer: { 27 | CHANNEL_OUT_GAIN: [ 28 | { 29 | mixerMessage: '/dmxis/ch/{channel}', 30 | value: 0, 31 | type: 'f', 32 | min: 0, 33 | max: 1, 34 | zero: 0.75, 35 | }, 36 | ], 37 | CHANNEL_NAME: [ 38 | { 39 | mixerMessage: '/dmxis/ch/name/{channel}', 40 | value: 0, 41 | type: 'f', 42 | min: 0, 43 | max: 1, 44 | zero: 0.75, 45 | }, 46 | ], 47 | }, 48 | }, 49 | ], 50 | fader: { 51 | min: 0, 52 | max: 1, 53 | zero: 0.75, 54 | step: 0.01, 55 | }, 56 | meter: { 57 | min: 0, 58 | max: 1, 59 | zero: 0.75, 60 | test: 0.6, 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /shared/src/constants/mixerProtocols/LawoRelayVrx4.ts: -------------------------------------------------------------------------------- 1 | import { IMixerProtocol, emptyMixerMessage } from '../MixerProtocolInterface' 2 | 3 | export const LawoRelayVrx4: IMixerProtocol = { 4 | protocol: 'EMBER', 5 | label: 'Lawo Relay VRX4 - client', 6 | presetFileExtension: '', 7 | loadPresetCommand: [emptyMixerMessage()], 8 | MAX_UPDATES_PER_SECOND: 10, 9 | leadingZeros: false, //some OSC protocols needs channels to be 01, 02 etc. 10 | pingCommand: [emptyMixerMessage()], 11 | pingResponseCommand: [emptyMixerMessage()], 12 | pingTime: 0, //Bypass ping when pingTime is zero 13 | initializeCommands: [emptyMixerMessage()], 14 | channelTypes: [ 15 | { 16 | channelTypeName: 'CH', 17 | channelTypeColor: '#2f2f2f', 18 | fromMixer: { 19 | CHANNEL_OUT_GAIN: [ 20 | { 21 | mixerMessage: 22 | 'R3LAYVRX4/Ex/GUI/FaderSlot_{channel}/FaderPosition', 23 | value: 0, 24 | type: 'real', 25 | min: 0, 26 | max: 100, 27 | zero: 75, 28 | }, 29 | ], 30 | CHANNEL_VU: [emptyMixerMessage()], 31 | CHANNEL_VU_REDUCTION: [emptyMixerMessage()], 32 | CHANNEL_NAME: [ 33 | { 34 | mixerMessage: '', 35 | value: 0, 36 | type: 'real', 37 | min: -200, 38 | max: 20, 39 | zero: 0, 40 | }, 41 | ], 42 | PFL: [emptyMixerMessage()], 43 | NEXT_SEND: [emptyMixerMessage()], 44 | AUX_LEVEL: [emptyMixerMessage()], 45 | CHANNEL_MUTE_ON: [emptyMixerMessage()], 46 | CHANNEL_MUTE_OFF: [emptyMixerMessage()], 47 | }, 48 | toMixer: { 49 | CHANNEL_OUT_GAIN: [ 50 | { 51 | mixerMessage: 52 | 'R3LAYVRX4/Ex/GUI/FaderSlot_{channel}/FaderPosition', 53 | value: 0, 54 | type: 'real', 55 | min: 0, 56 | max: 100, 57 | zero: 0, 58 | }, 59 | ], 60 | CHANNEL_NAME: [ 61 | { 62 | mixerMessage: '', 63 | value: 0, 64 | type: 'real', 65 | min: -200, 66 | max: 20, 67 | zero: 0, 68 | }, 69 | ], 70 | PFL_ON: [emptyMixerMessage()], 71 | PFL_OFF: [emptyMixerMessage()], 72 | NEXT_SEND: [emptyMixerMessage()], 73 | AUX_LEVEL: [emptyMixerMessage()], 74 | CHANNEL_MUTE_ON: [emptyMixerMessage()], 75 | CHANNEL_MUTE_OFF: [emptyMixerMessage()], 76 | }, 77 | }, 78 | ], 79 | fader: { 80 | min: 0, 81 | max: 200, 82 | zero: 1300, 83 | step: 10, 84 | }, 85 | meter: { 86 | min: 0, 87 | max: 1, 88 | zero: 0.75, 89 | test: 0.6, 90 | }, 91 | } 92 | -------------------------------------------------------------------------------- /shared/src/constants/mixerProtocols/SSLsystemT.ts: -------------------------------------------------------------------------------- 1 | import { IMixerProtocol, emptyMixerMessage } from '../MixerProtocolInterface' 2 | 3 | export const SSLSystemT: IMixerProtocol = { 4 | protocol: 'SSL', 5 | label: 'SSL System T', 6 | presetFileExtension: '', 7 | loadPresetCommand: [emptyMixerMessage()], 8 | MAX_UPDATES_PER_SECOND: 10, 9 | leadingZeros: false, 10 | pingCommand: [emptyMixerMessage()], 11 | pingResponseCommand: [emptyMixerMessage()], 12 | pingTime: 5000, 13 | initializeCommands: [ 14 | { 15 | mixerMessage: 'f1 04 00 00 00 {channel}', 16 | }, 17 | ], 18 | channelTypes: [ 19 | { 20 | channelTypeName: 'CH', 21 | channelTypeColor: '#2f2f2f', 22 | fromMixer: { 23 | CHANNEL_OUT_GAIN: [emptyMixerMessage()], // Handled by SSLMixerconnection 24 | CHANNEL_MUTE_ON: [ 25 | { 26 | mixerMessage: 'f1 04 00 01 00 {channel}', 27 | }, 28 | ], 29 | }, 30 | toMixer: { 31 | CHANNEL_OUT_GAIN: [ 32 | { 33 | mixerMessage: 'f1 06 00 80 00 {channel} {level}', 34 | }, 35 | ], 36 | PFL_ON: [ 37 | { 38 | mixerMessage: 'f1 05 00 80 05 {channel} 01', 39 | }, 40 | ], 41 | PFL_OFF: [ 42 | { 43 | mixerMessage: 'f1 05 00 80 05 {channel} 00', 44 | }, 45 | ], 46 | NEXT_SEND: [ 47 | { 48 | mixerMessage: 'f1 06 00 80 00 {channel} {level}', 49 | }, 50 | ], 51 | CHANNEL_MUTE_ON: [ 52 | { 53 | mixerMessage: 'f1 05 00 80 01 {channel} 00', 54 | }, 55 | ], 56 | CHANNEL_MUTE_OFF: [ 57 | { 58 | mixerMessage: 'f1 05 00 80 01 {channel} 01', 59 | }, 60 | ], 61 | }, 62 | }, 63 | ], 64 | fader: { 65 | min: 0, 66 | max: 1, 67 | zero: 0.75, 68 | step: 0.01, 69 | }, 70 | meter: { 71 | min: 0, 72 | max: 1, 73 | zero: 0.75, 74 | test: 0.6, 75 | }, 76 | } 77 | -------------------------------------------------------------------------------- /shared/src/constants/mixerProtocols/ardourMaster.ts: -------------------------------------------------------------------------------- 1 | import { IMixerProtocol } from '../MixerProtocolInterface' 2 | 3 | export const ArdourMaster: IMixerProtocol = { 4 | protocol: 'OSC', 5 | label: 'Ardour DAW - Master Mode', 6 | presetFileExtension: '', 7 | MAX_UPDATES_PER_SECOND: 10, 8 | leadingZeros: false, 9 | pingCommand: [ 10 | { 11 | mixerMessage: '/strip/list', 12 | value: 0, 13 | type: 'i', 14 | min: 0, 15 | max: 1, 16 | zero: 0.75, 17 | }, 18 | ], 19 | pingResponseCommand: [ 20 | { 21 | mixerMessage: '/strip/list', 22 | value: 0, 23 | type: 'i', 24 | min: 0, 25 | max: 1, 26 | zero: 0.75, 27 | }, 28 | ], 29 | pingTime: 9500, 30 | initializeCommands: [ 31 | { 32 | mixerMessage: '/set_surface/feedback', 33 | value: '135', 34 | type: 'i', 35 | min: 0, 36 | max: 1, 37 | zero: 0.75, 38 | }, 39 | ], 40 | channelTypes: [ 41 | { 42 | channelTypeName: 'CH', 43 | channelTypeColor: '#2f2f2f', 44 | fromMixer: { 45 | CHANNEL_OUT_GAIN: [ 46 | { 47 | mixerMessage: '/strip/fader/{channel}', 48 | value: 0, 49 | type: 'f', 50 | min: 0, 51 | max: 1, 52 | zero: 0.75, 53 | }, 54 | ], 55 | CHANNEL_VU: [ 56 | { 57 | mixerMessage: '/strip/meter/{channel}', 58 | value: 0, 59 | type: 'f', 60 | min: 0, 61 | max: 1, 62 | zero: 0.75, 63 | }, 64 | ], 65 | CHANNEL_NAME: [ 66 | { 67 | mixerMessage: '/strip/name/{channel}', 68 | value: 0, 69 | type: 'f', 70 | min: 0, 71 | max: 1, 72 | zero: 0.75, 73 | }, 74 | ], 75 | }, 76 | toMixer: { 77 | CHANNEL_OUT_GAIN: [ 78 | { 79 | mixerMessage: '/strip/fader/{channel}', 80 | value: 0, 81 | type: 'f', 82 | min: 0, 83 | max: 1, 84 | zero: 0.75, 85 | }, 86 | ], 87 | CHANNEL_NAME: [ 88 | { 89 | mixerMessage: '/strip/name/{channel}', 90 | value: 0, 91 | type: 'f', 92 | min: 0, 93 | max: 1, 94 | zero: 0.75, 95 | }, 96 | ], 97 | }, 98 | }, 99 | ], 100 | fader: { 101 | min: 0, 102 | max: 1, 103 | zero: 0.75, 104 | step: 0.01, 105 | }, 106 | meter: { 107 | min: 0, 108 | max: 1, 109 | zero: 0.85, 110 | test: 0.75, 111 | }, 112 | } 113 | -------------------------------------------------------------------------------- /shared/src/constants/mixerProtocols/atem.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IMixerProtocol, 3 | fxParamsList, 4 | VuLabelConversionType, 5 | } from '../MixerProtocolInterface' 6 | 7 | export const Atem: IMixerProtocol = { 8 | MAX_UPDATES_PER_SECOND: 10, 9 | protocol: 'ATEM', 10 | label: 'ATEM Audio Control', 11 | vuLabelConversionType: VuLabelConversionType.Decibel, 12 | vuLabelValues: [0, 0.125, 0.25, 0.375, 0.5, 0.75, 1], 13 | fader: { 14 | min: 0, 15 | max: 1, 16 | zero: 0.75, 17 | step: 0.01, 18 | }, 19 | channelTypes: [ 20 | { 21 | channelTypeName: 'Input', 22 | channelTypeColor: '#2f2f2f', 23 | fromMixer: { 24 | CHANNEL_INPUT_GAIN: [ 25 | { mixerMessage: '', maxLabel: 6, minLabel: -12 }, 26 | ], 27 | CHANNEL_INPUT_SELECTOR: [ 28 | { 29 | mixerMessage: '', 30 | label: 'LR', 31 | }, 32 | { 33 | mixerMessage: '', 34 | label: 'LL', 35 | }, 36 | { 37 | mixerMessage: '', 38 | label: 'RR', 39 | }, 40 | ], 41 | // CHANNEL_OUT_GAIN: [{ mixerMessage: '' }], 42 | // CHANNEL_VU?: Array 43 | // CHANNEL_VU_REDUCTION?: Array 44 | // CHANNEL_NAME?: Array 45 | // PFL?: Array 46 | // NEXT_SEND?: Array 47 | // [FX_PARAM: number]: Array 48 | // AUX_LEVEL?: Array 49 | // CHANNEL_MUTE_ON: [{ mixerMessage: '' }], 50 | // CHANNEL_MUTE_OFF?: Array 51 | // CHANNEL_AMIX?: Array 52 | }, 53 | toMixer: { 54 | CHANNEL_INPUT_GAIN: [{ mixerMessage: '' }], 55 | CHANNEL_INPUT_SELECTOR: [ 56 | { 57 | mixerMessage: '', 58 | label: 'LR', 59 | }, 60 | { 61 | mixerMessage: '', 62 | label: 'LL', 63 | }, 64 | { 65 | mixerMessage: '', 66 | label: 'RR', 67 | }, 68 | ], 69 | // CHANNEL_OUT_GAIN: [{ mixerMessage: '' }], 70 | // CHANNEL_NAME?: Array 71 | // PFL_ON?: Array 72 | // PFL_OFF?: Array 73 | // NEXT_SEND?: Array 74 | // [FX_PARAM: number]: Array 75 | // AUX_LEVEL?: Array 76 | CHANNEL_MUTE_ON: [{ mixerMessage: '' }], 77 | CHANNEL_MUTE_OFF: [{ mixerMessage: '' }], 78 | // CHANNEL_AMIX?: Array 79 | }, 80 | }, 81 | ], 82 | } 83 | -------------------------------------------------------------------------------- /shared/src/constants/mixerProtocols/behringerXrMaster.ts: -------------------------------------------------------------------------------- 1 | import { IMixerProtocol } from '../MixerProtocolInterface' 2 | 3 | export const BehringerXrMaster: IMixerProtocol = { 4 | protocol: 'OSC', 5 | label: 'Behringer XR-series / Midas MR-series', 6 | presetFileExtension: '', 7 | MAX_UPDATES_PER_SECOND: 10, 8 | leadingZeros: true, 9 | pingCommand: [ 10 | { 11 | mixerMessage: '/xremote', 12 | }, 13 | { 14 | mixerMessage: '/meters', 15 | value: '/meters/1', 16 | type: 's', 17 | }, 18 | { 19 | mixerMessage: '/meters', 20 | value: '/meters/6', 21 | type: 's', 22 | }, 23 | ], 24 | pingTime: 9500, 25 | initializeCommands: [ 26 | { 27 | mixerMessage: '/ch/{channel}/mix/fader', 28 | }, 29 | { 30 | mixerMessage: '/ch/{channel}/config/name', 31 | }, 32 | { 33 | mixerMessage: '/ch/{channel}/mix/{argument}/level', 34 | type: 'aux', 35 | }, 36 | { 37 | mixerMessage: '/ch/{channel}/dyn/thr', 38 | }, 39 | { 40 | mixerMessage: '/ch/{channel}/dyn/ratio', 41 | }, 42 | { 43 | mixerMessage: '/ch/{channel}/delay/time', 44 | }, 45 | { 46 | mixerMessage: '/ch/{channel}/eq/1/g', 47 | }, 48 | { 49 | mixerMessage: '/ch/{channel}/eq/2/g', 50 | }, 51 | { 52 | mixerMessage: '/ch/{channel}/eq/3/g', 53 | }, 54 | { 55 | mixerMessage: '/ch/{channel}/eq/4/g', 56 | }, 57 | ], 58 | channelTypes: [ 59 | { 60 | channelTypeName: 'CH', 61 | channelTypeColor: '#2f2f2f', 62 | fromMixer: { 63 | CHANNEL_OUT_GAIN: [ 64 | { 65 | mixerMessage: '/ch/{channel}/mix/fader', 66 | }, 67 | ], 68 | CHANNEL_VU: [ 69 | { 70 | mixerMessage: '/meters/1', 71 | }, 72 | ], 73 | CHANNEL_VU_REDUCTION: [ 74 | { 75 | mixerMessage: '/meters/6', 76 | }, 77 | ], 78 | CHANNEL_NAME: [ 79 | { 80 | mixerMessage: '/ch/{channel}/config/name', 81 | }, 82 | ], 83 | AUX_LEVEL: [ 84 | { 85 | mixerMessage: '/ch/{channel}/mix/{argument}/level', 86 | }, 87 | ], 88 | CHANNEL_MUTE_ON: [ 89 | { 90 | mixerMessage: '/ch/{channel}/mix/on', 91 | }, 92 | ], 93 | }, 94 | toMixer: { 95 | CHANNEL_OUT_GAIN: [ 96 | { 97 | mixerMessage: '/ch/{channel}/mix/fader', 98 | }, 99 | ], 100 | CHANNEL_NAME: [ 101 | { 102 | mixerMessage: '/ch/{channel}/config/name', 103 | }, 104 | ], 105 | AUX_LEVEL: [ 106 | { 107 | mixerMessage: '/ch/{channel}/mix/{argument}/level', 108 | }, 109 | ], 110 | CHANNEL_MUTE_ON: [ 111 | { 112 | mixerMessage: '/ch/{channel}/mix/on', 113 | value: 0, 114 | type: 'f', 115 | }, 116 | ], 117 | CHANNEL_MUTE_OFF: [ 118 | { 119 | mixerMessage: '/ch/{channel}/mix/on', 120 | value: 1, 121 | type: 'f', 122 | }, 123 | ], 124 | }, 125 | }, 126 | ], 127 | fader: { 128 | min: 0, 129 | max: 1, 130 | zero: 0.75, 131 | step: 0.01, 132 | }, 133 | meter: { 134 | min: 0, 135 | max: 1, 136 | zero: 0.75, 137 | test: 0.6, 138 | }, 139 | } 140 | -------------------------------------------------------------------------------- /shared/src/constants/mixerProtocols/genericMidi.ts: -------------------------------------------------------------------------------- 1 | import { IMixerProtocol, emptyMixerMessage } from '../MixerProtocolInterface' 2 | 3 | export const GenericMidi: IMixerProtocol = { 4 | protocol: 'MIDI', 5 | label: 'Generic Midi', 6 | presetFileExtension: '', 7 | loadPresetCommand: [emptyMixerMessage()], 8 | MAX_UPDATES_PER_SECOND: 10, 9 | leadingZeros: false, 10 | pingCommand: [emptyMixerMessage()], 11 | pingResponseCommand: [emptyMixerMessage()], 12 | pingTime: 0, 13 | initializeCommands: [emptyMixerMessage()], 14 | channelTypes: [ 15 | { 16 | channelTypeName: 'CH', 17 | channelTypeColor: '#2f2f2f', 18 | fromMixer: { 19 | CHANNEL_OUT_GAIN: [ 20 | { 21 | mixerMessage: '0', 22 | value: 0, 23 | type: 'f', 24 | min: 0, 25 | max: 1, 26 | zero: 0.75, 27 | }, 28 | ], //PgmChange 0 - ignores this command 29 | CHANNEL_VU: [ 30 | { 31 | mixerMessage: '0', 32 | value: 0, 33 | type: 'f', 34 | min: 0, 35 | max: 1, 36 | zero: 0.75, 37 | }, 38 | ], //PgmChange 0 - ignores this command 39 | CHANNEL_VU_REDUCTION: [emptyMixerMessage()], 40 | CHANNEL_NAME: [emptyMixerMessage()], 41 | PFL: [emptyMixerMessage()], 42 | NEXT_SEND: [emptyMixerMessage()], 43 | AUX_LEVEL: [emptyMixerMessage()], 44 | CHANNEL_MUTE_ON: [emptyMixerMessage()], 45 | CHANNEL_MUTE_OFF: [emptyMixerMessage()], 46 | }, 47 | toMixer: { 48 | CHANNEL_OUT_GAIN: [ 49 | { 50 | mixerMessage: '38', 51 | value: 0, 52 | type: 'f', 53 | min: 0, 54 | max: 1, 55 | zero: 0.75, 56 | }, 57 | ], 58 | CHANNEL_NAME: [emptyMixerMessage()], 59 | PFL_ON: [emptyMixerMessage()], 60 | PFL_OFF: [emptyMixerMessage()], 61 | NEXT_SEND: [emptyMixerMessage()], 62 | AUX_LEVEL: [emptyMixerMessage()], 63 | CHANNEL_MUTE_ON: [emptyMixerMessage()], 64 | CHANNEL_MUTE_OFF: [emptyMixerMessage()], 65 | }, 66 | }, 67 | ], 68 | fader: { 69 | min: 0, 70 | max: 127, 71 | zero: 100, 72 | step: 1, 73 | }, 74 | meter: { 75 | min: 0, 76 | max: 127, 77 | zero: 100, 78 | test: 80, 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /shared/src/constants/mixerProtocols/reaperMaster.ts: -------------------------------------------------------------------------------- 1 | import { IMixerProtocol } from '../MixerProtocolInterface' 2 | 3 | export const ReaperMaster: IMixerProtocol = { 4 | protocol: 'OSC', 5 | label: 'Reaper DAW Master mode(reaper.fm)', 6 | presetFileExtension: '', 7 | MAX_UPDATES_PER_SECOND: 10, 8 | leadingZeros: false, //some OSC protocols needs channels to be 01, 02 etc. 9 | pingTime: 0, //Set to value to get MixerOnline status 10 | channelTypes: [ 11 | { 12 | channelTypeName: 'CH', 13 | channelTypeColor: '#2f2f2f', 14 | fromMixer: { 15 | CHANNEL_OUT_GAIN: [ 16 | { 17 | mixerMessage: '/track/{channel}/volume', 18 | value: 0, 19 | type: 'f', 20 | min: 0, 21 | max: 1, 22 | zero: 0.75, 23 | }, 24 | ], 25 | CHANNEL_VU: [ 26 | { 27 | mixerMessage: '/track/{channel}/vu', 28 | value: 0, 29 | type: 'f', 30 | min: 0, 31 | max: 1, 32 | zero: 0.75, 33 | }, 34 | ], 35 | CHANNEL_NAME: [ 36 | { 37 | mixerMessage: '/track/{channel}/name', 38 | value: 0, 39 | type: 'f', 40 | min: 0, 41 | max: 1, 42 | zero: 0.75, 43 | }, 44 | ], 45 | }, 46 | toMixer: { 47 | CHANNEL_OUT_GAIN: [ 48 | { 49 | mixerMessage: '/track/{channel}/volume', 50 | value: 0, 51 | type: 'f', 52 | min: 0, 53 | max: 1, 54 | zero: 0.75, 55 | }, 56 | ], 57 | CHANNEL_NAME: [ 58 | { 59 | mixerMessage: '/track/{channel}/name', 60 | value: 0, 61 | type: 'f', 62 | min: 0, 63 | max: 1, 64 | zero: 0.75, 65 | }, 66 | ], 67 | PFL_ON: [ 68 | { 69 | mixerMessage: '/track/{channel}/solo', 70 | value: 1, 71 | type: 'i', 72 | min: 0, 73 | max: 1, 74 | zero: 0.75, 75 | }, 76 | ], 77 | PFL_OFF: [ 78 | { 79 | mixerMessage: '/track/{channel}/solo', 80 | value: 0, 81 | type: 'i', 82 | min: 0, 83 | max: 1, 84 | zero: 0.75, 85 | }, 86 | ], 87 | }, 88 | }, 89 | { 90 | channelTypeName: 'MASTER', 91 | channelTypeColor: '#0f0f3f', 92 | fromMixer: { 93 | CHANNEL_OUT_GAIN: [ 94 | { 95 | mixerMessage: '/master/volume', 96 | value: 0, 97 | type: 'f', 98 | min: 0, 99 | max: 1, 100 | zero: 0.75, 101 | }, 102 | ], 103 | CHANNEL_VU: [ 104 | { 105 | mixerMessage: '/master/vu/L', 106 | value: 0, 107 | type: 'f', 108 | min: 0, 109 | max: 1, 110 | zero: 0.75, 111 | }, 112 | { 113 | mixerMessage: '/master/vu/R', 114 | value: 0, 115 | type: 'f', 116 | min: 0, 117 | max: 1, 118 | zero: 0.75, 119 | }, 120 | ], 121 | }, 122 | toMixer: { 123 | CHANNEL_OUT_GAIN: [ 124 | { 125 | mixerMessage: '/master/volume', 126 | value: 0, 127 | type: 'f', 128 | min: 0, 129 | max: 1, 130 | zero: 0.75, 131 | }, 132 | ], 133 | }, 134 | }, 135 | ], 136 | fader: { 137 | min: 0, 138 | max: 1, 139 | zero: 0.75, 140 | step: 0.01, 141 | }, 142 | meter: { 143 | min: 0, 144 | max: 1, 145 | zero: 0.75, 146 | test: 0.6, 147 | }, 148 | } 149 | -------------------------------------------------------------------------------- /shared/src/constants/mixerProtocols/yamahaQLCL.ts: -------------------------------------------------------------------------------- 1 | import { IMixerProtocol, emptyMixerMessage } from '../MixerProtocolInterface' 2 | 3 | export const YamahaQLCL: IMixerProtocol = { 4 | protocol: 'QLCL', 5 | label: 'Yamaha QL/CL', 6 | presetFileExtension: '', 7 | loadPresetCommand: [emptyMixerMessage()], 8 | MAX_UPDATES_PER_SECOND: 10, 9 | leadingZeros: false, 10 | pingCommand: [emptyMixerMessage()], 11 | pingResponseCommand: [emptyMixerMessage()], 12 | pingTime: 10000, 13 | initializeCommands: [ 14 | { 15 | mixerMessage: 'f0 43 30 3e 19 01 00 37 00 00 {channel} f7', 16 | value: 0, 17 | type: '', 18 | min: 0, 19 | max: 1, 20 | zero: 0.75, 21 | }, 22 | ], 23 | channelTypes: [ 24 | { 25 | channelTypeName: 'CH', 26 | channelTypeColor: '#2f2f2f', 27 | fromMixer: { 28 | CHANNEL_OUT_GAIN: [ 29 | { 30 | mixerMessage: 31 | 'f0 43 10 3e 19 01 00 37 00 00 {channel} 00 00 00 {level} f7', 32 | value: 0, 33 | type: '', 34 | min: 0, 35 | max: 1, 36 | zero: 0.75, 37 | }, 38 | ], //PgmChange 0 - ignores this command 39 | CHANNEL_VU: [ 40 | { 41 | mixerMessage: '0', 42 | value: 0, 43 | type: 'f', 44 | min: 0, 45 | max: 1, 46 | zero: 0.75, 47 | }, 48 | ], //PgmChange 0 - ignores this command 49 | CHANNEL_VU_REDUCTION: [emptyMixerMessage()], 50 | CHANNEL_NAME: [emptyMixerMessage()], 51 | PFL: [emptyMixerMessage()], 52 | NEXT_SEND: [emptyMixerMessage()], 53 | AUX_LEVEL: [emptyMixerMessage()], 54 | CHANNEL_MUTE_ON: [ 55 | { 56 | mixerMessage: 57 | 'f0 43 10 3e 19 01 00 35 00 00 {channel} 00 00 00 00 00 f7', 58 | value: 0, 59 | type: '', 60 | min: 0, 61 | max: 1, 62 | zero: 0.75, 63 | }, 64 | ], 65 | // Only MUTE_ON is used as receiver 66 | CHANNEL_MUTE_OFF: [emptyMixerMessage()], 67 | }, 68 | toMixer: { 69 | CHANNEL_OUT_GAIN: [ 70 | { 71 | mixerMessage: 72 | 'f0 43 10 3e 19 01 00 37 00 00 {channel} 00 00 00 {level} f7', 73 | value: 0, 74 | type: '', 75 | min: 0, 76 | max: 1, 77 | zero: 0.75, 78 | }, 79 | ], 80 | CHANNEL_NAME: [emptyMixerMessage()], 81 | PFL_ON: [emptyMixerMessage()], 82 | PFL_OFF: [emptyMixerMessage()], 83 | NEXT_SEND: [emptyMixerMessage()], 84 | AUX_LEVEL: [emptyMixerMessage()], 85 | CHANNEL_MUTE_ON: [ 86 | { 87 | mixerMessage: 88 | 'f0 43 10 3e 19 01 00 35 00 00 {channel} 00 00 00 00 00 f7', 89 | value: 0, 90 | type: '', 91 | min: 0, 92 | max: 1, 93 | zero: 0.75, 94 | }, 95 | ], 96 | CHANNEL_MUTE_OFF: [ 97 | { 98 | mixerMessage: 99 | 'f0 43 10 3e 19 01 00 35 00 00 {channel} 00 00 00 00 01 f7', 100 | value: 0, 101 | type: '', 102 | min: 0, 103 | max: 1, 104 | zero: 0.75, 105 | }, 106 | ], 107 | }, 108 | }, 109 | ], 110 | fader: { 111 | min: 0, 112 | max: 1, 113 | zero: 0.75, 114 | step: 0.01, 115 | }, 116 | meter: { 117 | min: 0, 118 | max: 1, 119 | zero: 0.75, 120 | test: 0.6, 121 | }, 122 | } 123 | -------------------------------------------------------------------------------- /shared/src/constants/remoteProtocols/HuiRemoteFaderPresets.ts: -------------------------------------------------------------------------------- 1 | export interface IMidiSendMessage { 2 | message: string 3 | value: any 4 | type: MidiSendTypes 5 | } 6 | 7 | export enum MidiSendTypes { 8 | disabled, 9 | playNote, 10 | stopNote, 11 | sendControlChange, 12 | sendPitchBend, 13 | } 14 | 15 | export interface IMidiReceiveMessage { 16 | message: string 17 | value: any 18 | type: MidiReceiveTypes 19 | } 20 | export enum MidiReceiveTypes { 21 | disabled, 22 | noteon, 23 | noteoff, 24 | controlchange, 25 | pitchbend, 26 | } 27 | 28 | export interface IRemoteProtocol { 29 | protocol: string 30 | label: string 31 | mode: string 32 | leadingZeros: boolean 33 | initializeCommands: [IMidiSendMessage] 34 | fromRemote: { 35 | CHANNEL_PGM_ON_OFF: IMidiReceiveMessage 36 | CHANNEL_PST_ON_OFF: IMidiReceiveMessage 37 | CHANNEL_PFL_ON_OFF: IMidiReceiveMessage 38 | CHANNEL_FADER_LEVEL: IMidiReceiveMessage 39 | X_MIX: IMidiReceiveMessage 40 | FADE_TO_BLACK: IMidiReceiveMessage 41 | SNAP_RECALL: IMidiReceiveMessage 42 | } 43 | toRemote: { 44 | STATE_CHANNEL_PGM: IMidiSendMessage 45 | STATE_CHANNEL_PST: IMidiSendMessage 46 | STATE_CHANNEL_PFL: IMidiSendMessage 47 | STATE_CHANNEL_FADER_LEVEL: Array 48 | } 49 | fader: { 50 | min: number 51 | max: number 52 | zero: number 53 | step: number 54 | } 55 | meter: { 56 | min: number 57 | max: number 58 | zero: number 59 | test: number 60 | } 61 | } 62 | 63 | export const RemoteFaderPresets: { [key: string]: IRemoteProtocol } = { 64 | hui: { 65 | protocol: 'MIDI', 66 | label: 'Generic HUI Midicontroller', 67 | mode: 'client', 68 | leadingZeros: true, 69 | initializeCommands: [ 70 | { 71 | message: '', 72 | value: '', 73 | type: MidiSendTypes.disabled, 74 | }, 75 | ], 76 | fromRemote: { 77 | CHANNEL_PGM_ON_OFF: { 78 | message: '', 79 | value: '', 80 | type: MidiReceiveTypes.disabled, 81 | }, 82 | CHANNEL_PST_ON_OFF: { 83 | message: '', 84 | value: '', 85 | type: MidiReceiveTypes.disabled, 86 | }, 87 | CHANNEL_PFL_ON_OFF: { 88 | message: '', 89 | value: '', 90 | type: MidiReceiveTypes.disabled, 91 | }, 92 | CHANNEL_FADER_LEVEL: { 93 | message: '0', 94 | value: '', 95 | type: MidiReceiveTypes.controlchange, 96 | }, 97 | X_MIX: { 98 | message: '', 99 | value: '', 100 | type: MidiReceiveTypes.disabled, 101 | }, 102 | FADE_TO_BLACK: { 103 | message: '', 104 | value: '', 105 | type: MidiReceiveTypes.disabled, 106 | }, 107 | SNAP_RECALL: { 108 | message: '', 109 | value: '', 110 | type: MidiReceiveTypes.disabled, 111 | }, 112 | }, 113 | toRemote: { 114 | STATE_CHANNEL_PGM: { 115 | message: '', 116 | value: '', 117 | type: MidiSendTypes.disabled, 118 | }, 119 | STATE_CHANNEL_PST: { 120 | message: '', 121 | value: '', 122 | type: MidiSendTypes.disabled, 123 | }, 124 | STATE_CHANNEL_PFL: { 125 | message: '', 126 | value: '', 127 | type: MidiSendTypes.disabled, 128 | }, 129 | STATE_CHANNEL_FADER_LEVEL: [ 130 | { 131 | message: '21', 132 | value: '', 133 | type: MidiSendTypes.sendControlChange, 134 | }, 135 | { 136 | message: '01', 137 | value: '', 138 | type: MidiSendTypes.sendControlChange, 139 | }, 140 | ], 141 | }, 142 | fader: { 143 | min: 0, 144 | max: 127, 145 | zero: 70, 146 | step: 1, 147 | }, 148 | meter: { 149 | min: 0, 150 | max: 1, 151 | zero: 0.75, 152 | test: 0.6, 153 | }, 154 | }, 155 | } 156 | 157 | export const RemoteFaderProtocolList = Object.getOwnPropertyNames( 158 | RemoteFaderPresets 159 | ).map((preset) => { 160 | return { 161 | value: preset, 162 | label: RemoteFaderPresets[preset].label, 163 | } 164 | }) 165 | -------------------------------------------------------------------------------- /shared/src/constants/remoteProtocols/SkaarhojProtocol.ts: -------------------------------------------------------------------------------- 1 | export interface IRawSendMessage { 2 | message: string 3 | value: any 4 | type: RawSendTypes 5 | } 6 | 7 | export enum RawSendTypes { 8 | disabled, 9 | playNote, 10 | stopNote, 11 | sendControlChange, 12 | sendPitchBend, 13 | } 14 | 15 | export interface IRawReceiveMessage { 16 | message: string 17 | value: any 18 | type: RawReceiveTypes 19 | } 20 | export enum RawReceiveTypes { 21 | disabled, 22 | noteon, 23 | noteoff, 24 | controlchange, 25 | pitchbend, 26 | } 27 | 28 | export interface IRemoteProtocol { 29 | protocol: string 30 | label: string 31 | mode: string 32 | leadingZeros: boolean 33 | initializeCommands: [IRawSendMessage] 34 | fromRemote: { 35 | CHANNEL_PGM_ON_OFF: IRawReceiveMessage 36 | CHANNEL_PST_ON_OFF: IRawReceiveMessage 37 | CHANNEL_PFL_ON_OFF: IRawReceiveMessage 38 | CHANNEL_FADER_LEVEL: IRawReceiveMessage 39 | X_MIX: IRawReceiveMessage 40 | FADE_TO_BLACK: IRawReceiveMessage 41 | SNAP_RECALL: IRawReceiveMessage 42 | } 43 | toRemote: { 44 | STATE_CHANNEL_PGM: IRawSendMessage 45 | STATE_CHANNEL_PST: IRawSendMessage 46 | STATE_CHANNEL_PFL: IRawSendMessage 47 | STATE_CHANNEL_FADER_LEVEL: Array 48 | } 49 | fader: { 50 | min: number 51 | max: number 52 | zero: number 53 | step: number 54 | } 55 | meter: { 56 | min: number 57 | max: number 58 | zero: number 59 | test: number 60 | } 61 | } 62 | 63 | export const RemoteFaderPresets: { [key: string]: IRemoteProtocol } = { 64 | rawPanel: { 65 | protocol: 'RAW', 66 | label: 'Generic Skaarhoj Protocol', 67 | mode: 'client', 68 | leadingZeros: false, 69 | initializeCommands: [ 70 | { 71 | message: '', 72 | value: '', 73 | type: RawSendTypes.disabled, 74 | }, 75 | ], 76 | fromRemote: { 77 | CHANNEL_PGM_ON_OFF: { 78 | message: '', 79 | value: '', 80 | type: RawReceiveTypes.disabled, 81 | }, 82 | CHANNEL_PST_ON_OFF: { 83 | message: '', 84 | value: '', 85 | type: RawReceiveTypes.disabled, 86 | }, 87 | CHANNEL_PFL_ON_OFF: { 88 | message: '', 89 | value: '', 90 | type: RawReceiveTypes.disabled, 91 | }, 92 | CHANNEL_FADER_LEVEL: { 93 | message: '0', 94 | value: '', 95 | type: RawReceiveTypes.controlchange, 96 | }, 97 | X_MIX: { 98 | message: '', 99 | value: '', 100 | type: RawReceiveTypes.disabled, 101 | }, 102 | FADE_TO_BLACK: { 103 | message: '', 104 | value: '', 105 | type: RawReceiveTypes.disabled, 106 | }, 107 | SNAP_RECALL: { 108 | message: '', 109 | value: '', 110 | type: RawReceiveTypes.disabled, 111 | }, 112 | }, 113 | toRemote: { 114 | STATE_CHANNEL_PGM: { 115 | message: '', 116 | value: '', 117 | type: RawSendTypes.disabled, 118 | }, 119 | STATE_CHANNEL_PST: { 120 | message: '', 121 | value: '', 122 | type: RawSendTypes.disabled, 123 | }, 124 | STATE_CHANNEL_PFL: { 125 | message: '', 126 | value: '', 127 | type: RawSendTypes.disabled, 128 | }, 129 | STATE_CHANNEL_FADER_LEVEL: [ 130 | { 131 | message: '21', 132 | value: '', 133 | type: RawSendTypes.sendControlChange, 134 | }, 135 | { 136 | message: '01', 137 | value: '', 138 | type: RawSendTypes.sendControlChange, 139 | }, 140 | ], 141 | }, 142 | fader: { 143 | min: 0, 144 | max: 127, 145 | zero: 70, 146 | step: 1, 147 | }, 148 | meter: { 149 | min: 0, 150 | max: 1, 151 | zero: 0.75, 152 | test: 0.6, 153 | }, 154 | }, 155 | } 156 | 157 | export const RemoteFaderProtocolList = Object.getOwnPropertyNames( 158 | RemoteFaderPresets 159 | ).map((preset) => { 160 | return { 161 | value: preset, 162 | label: RemoteFaderPresets[preset].label, 163 | } 164 | }) 165 | -------------------------------------------------------------------------------- /shared/src/reducers/indexReducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { channels } from './channelsReducer' 3 | import { settings } from './settingsReducer' 4 | import { faders } from './fadersReducer' 5 | 6 | const indexReducer = combineReducers({ 7 | faders, 8 | channels, 9 | settings, 10 | }) 11 | 12 | export default indexReducer 13 | -------------------------------------------------------------------------------- /shared/src/reducers/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | import { IFaders } from './fadersReducer' 3 | import { IChannels } from './channelsReducer' 4 | import indexReducer from './indexReducer' 5 | import { ISettings } from './settingsReducer' 6 | 7 | export interface IStore { 8 | settings: Array 9 | channels: Array 10 | faders: Array 11 | } 12 | 13 | export default configureStore({reducer: indexReducer}) 14 | export { Store } from 'redux' -------------------------------------------------------------------------------- /shared/src/utils/vu-server-types.ts: -------------------------------------------------------------------------------- 1 | export enum VuType { 2 | Channel = 'vuChannel', 3 | Reduction = 'vuReduction', 4 | } -------------------------------------------------------------------------------- /storage/Default.x32: -------------------------------------------------------------------------------- 1 | { 2 | "sceneIndex": "0", 3 | "sceneName": "default" 4 | } 5 | -------------------------------------------------------------------------------- /storage/Preset 1.x32: -------------------------------------------------------------------------------- 1 | { 2 | "sceneIndex": "1", 3 | "sceneName": "Preset 1" 4 | } 5 | -------------------------------------------------------------------------------- /storage/Preset 2.x32: -------------------------------------------------------------------------------- 1 | { 2 | "sceneIndex": "2", 3 | "sceneName": "Preset 2" 4 | } 5 | -------------------------------------------------------------------------------- /storage/Preset 3.x32: -------------------------------------------------------------------------------- 1 | { 2 | "sceneIndex": "3", 3 | "sceneName": "Preset 3" 4 | } 5 | -------------------------------------------------------------------------------- /storage/default-casparcg.ccg: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Sofie CasparCG Example", 3 | "fromMixer": { 4 | "CHANNEL_VU": [ 5 | [ 6 | "/channel/1/stage/layer/51/audio/1/pFS", 7 | "/channel/1/stage/layer/51/audio/2/pFS" 8 | ], 9 | [ 10 | "/channel/1/stage/layer/52/audio/1/pFS", 11 | "/channel/1/stage/layer/52/audio/2/pFS" 12 | ], 13 | [ 14 | "/channel/1/stage/layer/53/audio/1/pFS", 15 | "/channel/1/stage/layer/53/audio/2/pFS" 16 | ], 17 | [ 18 | "/channel/1/stage/layer/54/audio/1/pFS", 19 | "/channel/1/stage/layer/54/audio/2/pFS" 20 | ], 21 | [ 22 | "/channel/1/stage/layer/55/audio/1/pFS", 23 | "/channel/1/stage/layer/55/audio/2/pFS" 24 | ], 25 | [ 26 | "/channel/1/stage/layer/56/audio/1/pFS", 27 | "/channel/1/stage/layer/56/audio/2/pFS" 28 | ] 29 | ] 30 | }, 31 | "toMixer": { 32 | "PFL_AUX_FADER_LEVEL": [ 33 | [ 34 | { 35 | "channel": 2, 36 | "layer": 51 37 | } 38 | ], 39 | [ 40 | { 41 | "channel": 2, 42 | "layer": 52 43 | } 44 | ], 45 | [ 46 | { 47 | "channel": 2, 48 | "layer": 53 49 | } 50 | ], 51 | [ 52 | { 53 | "channel": 2, 54 | "layer": 54 55 | } 56 | ], 57 | [ 58 | { 59 | "channel": 2, 60 | "layer": 55 61 | } 62 | ], 63 | [ 64 | { 65 | "channel": 2, 66 | "layer": 56 67 | } 68 | ] 69 | ], 70 | "PGM_CHANNEL_FADER_LEVEL": [ 71 | [ 72 | { 73 | "channel": 1, 74 | "layer": 51 75 | }, 76 | { 77 | "channel": 3, 78 | "layer": 51 79 | } 80 | ], 81 | [ 82 | { 83 | "channel": 1, 84 | "layer": 52 85 | }, 86 | { 87 | "channel": 3, 88 | "layer": 52 89 | } 90 | ], 91 | [ 92 | { 93 | "channel": 1, 94 | "layer": 53 95 | }, 96 | { 97 | "channel": 3, 98 | "layer": 53 99 | } 100 | ], 101 | [ 102 | { 103 | "channel": 1, 104 | "layer": 54 105 | }, 106 | { 107 | "channel": 3, 108 | "layer": 54 109 | } 110 | ], 111 | [ 112 | { 113 | "channel": 1, 114 | "layer": 55 115 | }, 116 | { 117 | "channel": 3, 118 | "layer": 55 119 | } 120 | ], 121 | [ 122 | { 123 | "channel": 1, 124 | "layer": 56 125 | }, 126 | { 127 | "channel": 3, 128 | "layer": 56 129 | } 130 | ] 131 | ], 132 | "CHANNEL_INPUT_SELECTOR": [ 133 | { 134 | "label": "1L-2R", 135 | "mixerMessage": "8ch2" 136 | }, 137 | { 138 | "label": "1L-1R", 139 | "mixerMessage": "4ch-dleft" 140 | }, 141 | { 142 | "label": "2L-2R", 143 | "mixerMessage": "4ch-dright" 144 | } 145 | ] 146 | }, 147 | "channelLabels": [ 148 | "RM1", 149 | "RM2", 150 | "RM3", 151 | "RM4", 152 | "RM5", 153 | "MP1" 154 | ], 155 | "sourceOptions": { 156 | "sources": [ 157 | { 158 | "channel": 2, 159 | "layer": 51 160 | }, 161 | { 162 | "channel": 2, 163 | "layer": 52 164 | }, 165 | { 166 | "channel": 2, 167 | "layer": 53 168 | }, 169 | { 170 | "channel": 2, 171 | "layer": 54 172 | }, 173 | { 174 | "channel": 2, 175 | "layer": 55 176 | }, 177 | { 178 | "channel": 2, 179 | "layer": 56 180 | } 181 | ], 182 | "options": { 183 | "CHANNEL_LAYOUT": { 184 | "1L-2R": "8ch2", 185 | "1L-1R": "4ch-dleft", 186 | "2L-2R": "4ch-dright" 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /storage/pages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "example", 4 | "label": "LIVE", 5 | "faders": [0, 2, 3, 4, 8, 13, 14, 15, 31] 6 | }, 7 | { "id": "studio", "label": "STUDIO", "faders": [0, 1, 4, 5, 9, 11] }, 8 | { "id": "custom2", "label": "PLAYOUT", "faders": [7, 8, 10] }, 9 | { "id": "custom3", "label": "Custom 3", "faders": [] } 10 | ] 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-plugin-prettier", 4 | "tslint:latest", 5 | "tslint-config-prettier" 6 | ], 7 | "rules": { 8 | "prettier": true, 9 | "interface-name": false, 10 | "object-literal-sort-keys": false, 11 | "variable-name": [ 12 | true, 13 | "ban-keywords", 14 | "check-format", 15 | "allow-leading-underscore", 16 | "require-const-for-all-caps" 17 | ], 18 | "no-bitwise": false 19 | } 20 | } 21 | --------------------------------------------------------------------------------