├── .dockerignore ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitsecret ├── keys │ ├── pubring.kbx │ ├── pubring.kbx~ │ └── trustdb.gpg └── paths │ └── mapping.cfg ├── Dockerfile ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── c3lingo_mumble ├── __init__.py ├── asio_singleton.py ├── audio.py ├── config.py ├── config_loader.py ├── manylisteners.py ├── mapping.py ├── mumble_client.py ├── play_wav.py ├── program_source.py ├── recv_pyaudio.py ├── recv_stdout.py ├── register.py ├── send_pyaudio.py ├── send_stdin.py ├── single_wrapper.py ├── stdchannel_redirected.py └── wrapper.py ├── certs ├── Saal_1-0-cert.pem ├── Saal_1-1-cert.pem ├── Saal_1-2-cert.pem ├── Saal_G-0-cert.pem ├── Saal_G-1-cert.pem ├── Saal_G-2-cert.pem ├── Saal_Z-0-cert.pem ├── Saal_Z-1-cert.pem ├── Saal_Z-2-cert.pem ├── stb-cert.pem └── test-cert.pem ├── examples ├── play_wav │ ├── c3lingo-test-channel.wav │ ├── divoc-test-1.yaml │ ├── divoc-test-2.yaml │ ├── saal_1.yaml │ └── test-channel.yaml ├── register.sh ├── send_pyaudio │ └── adams.yaml └── send_stdin │ ├── adams.yaml │ ├── mumblesender.service │ └── saal_1.yaml ├── gen-certs.sh └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.idea/misc.xml 3 | /.idea/workspace.xml 4 | /venv 5 | /*.wav 6 | certs/*-key.pem 7 | certs/*.p12 8 | __pycache__ 9 | Pipfile.lock 10 | 11 | .git 12 | .gitsecret 13 | certs 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Build the Docker image 18 | run: docker build . --file Dockerfile --tag c3lingo/c3lingo-mumble:latest --tag c3lingo/c3lingo-mumble:$(date +%s) 19 | - name: Log into registry 20 | run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin 21 | - name: Push to Docker Hub 22 | run: docker push c3lingo/c3lingo-mumble:latest 23 | # - name: Docker Hub Description 24 | # uses: peter-evans/dockerhub-description@v2 25 | # env: 26 | # DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} 27 | # DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} 28 | # DOCKERHUB_REPOSITORY: c3lingo/c3lingo-mumble -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.idea/misc.xml 3 | /.idea/workspace.xml 4 | /venv 5 | *~ 6 | *.pcm 7 | __pycache__ 8 | *.pyc 9 | *.wav 10 | certs/*-key.pem 11 | certs/*.p12 12 | certs/old 13 | .DS_Store 14 | 15 | .gitsecret/keys/random_seed 16 | !*.secret 17 | -------------------------------------------------------------------------------- /.gitsecret/keys/pubring.kbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c3lingo/c3lingo-mumble/20d5b8f1751c0c609d9d47341eb4b3804aef29dd/.gitsecret/keys/pubring.kbx -------------------------------------------------------------------------------- /.gitsecret/keys/pubring.kbx~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c3lingo/c3lingo-mumble/20d5b8f1751c0c609d9d47341eb4b3804aef29dd/.gitsecret/keys/pubring.kbx~ -------------------------------------------------------------------------------- /.gitsecret/keys/trustdb.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c3lingo/c3lingo-mumble/20d5b8f1751c0c609d9d47341eb4b3804aef29dd/.gitsecret/keys/trustdb.gpg -------------------------------------------------------------------------------- /.gitsecret/paths/mapping.cfg: -------------------------------------------------------------------------------- 1 | certs/borg-1-key.pem:62fdd990af06cba568eb043e1118aa8b10d636bc0ba49ab238fe7a068ed7e8ce 2 | certs/borg-2-key.pem:14ffe9202e975eb61535c85bf7ebf8adf9d5d80155c174c3a1d3e2ef4216025b 3 | certs/clarke-1-key.pem:c5a04538559ea71fb092e1c04f30747052f021e9ba68cdbf20cca0a44b4e1e6c 4 | certs/clarke-2-key.pem:481f75dd3cec3b03283ff3a382475627d027b18071869dc1fa424ea7a69e583e 5 | certs/dijkstra-1-key.pem:21ed52e8dce18f490e53941413fece2986cdd9edd15307bdf730c1ae4352afc4 6 | certs/dijkstra-2-key.pem:b78aafb10d8c252e0c914e6fc7c93f40377a48de37f2dd9ec35ab98ca9404442 7 | certs/eliza-1-key.pem:96d7bafdf57668638dc481f48d4dd31affbd39fa8705c9329dfc4583e2d00c2e 8 | certs/eliza-2-key.pem:fbf93a9cfe1c7cba300be2a455e13b0d2a035fbf444f109bf70f782bf99fb864 9 | certs/borg-0-key.pem:16b4cd6d4abe7a855dd2d9a685d95c078f3ed343e435a7578926a78c96d638b0 10 | certs/clarke-0-key.pem:de8d3794026529bd2026466ac9127bae011e301ab4c5b7ba8454a8f4d2c7088f 11 | certs/dijkstra-0-key.pem:214ccd603d8380c5168cb0e4d24587ef664872748004caaabdb2d7b9e01544af 12 | certs/eliza-0-key.pem:d117e86dfe5f100d4e2cb5662039aa1781a967b00c000361d53f92867742c7f0 13 | certs/test-key.pem:90a4f63d8c14fc4e48d2c76c2bbd1091311c615b054a7f93377853dab8e8231c 14 | certs/ada-0-key.pem:a13b5b56ce0adb44ada756c7c3372e9d93daa7655b999898627e8248d164319d 15 | certs/ada-1-key.pem:a34a5bf46a4f5bf50572497372885e982a4560fcf37bb09394fdea507e355081 16 | certs/ada-2-key.pem:8707f910fecbf0ee9684ac4364cfe49619bce7fa9bb43bbc79d594ca15d82c59 17 | certs/wikipaka-0-key.pem:037bf534a3960f86403a8482825b42e4ba8faac690125a644209b94e51c3e83b 18 | certs/wikipaka-1-key.pem:d9a8303581eb58bf8480aa46d576c245ea90443db8a98d18a39cff0b4eebf69e 19 | certs/wikipaka-2-key.pem:ccfb35e66ce4b28d4118d503083ce1ec543b8ea578846e0805c78e0d61387f01 20 | certs/wikipaka-0.p12:247cba5a3d7b2420004814d43296678d2516ccbc3c048cc06918c7125c03daf8 21 | certs/wikipaka-1.p12:64a99033e00f04ba397d36b85eddc77d8cfcc8df795080b74f8ab1efbd15cdad 22 | certs/wikipaka-2.p12:013ad1162b763d1953aa1c63d5c8347b5b1f8683d6282e5462cae0de1df44690 23 | certs/ada-0.p12:aed89107aa2d8d8093b8e52c536611c6edff2a6ca301746f285be3a3739e9514 24 | certs/ada-1.p12:630ba7abfb13d4844df966181deb716c50bceb363f7118a46f77c18d5a152f9f 25 | certs/ada-2.p12:fbcc20bda4967ca23e9e40e8643f036fb8437e3005c71e6ee6d7e5eb5883bbb8 26 | certs/saal23-0-key.pem:4fe78697581da3fc67a024721532aff3a389238102b57418bea057012dde8886 27 | certs/saal23-1-key.pem:64930f3009fd1496cd4d832b610ffadfe6fed202a5b7539b5188240997425b27 28 | certs/saal23-2-key.pem:f307eb9ace2dfbbbf0a4dd5c4d73feea5ccdf0557801a33f5f58b746bd1a5835 29 | certs/saal23-0.p12:f8173bc0221a45f4e9dfb9f9f575594f220d3efbeaf2096f0418d4251432200d 30 | certs/saal23-1.p12:e5bf69d87cd32b08324e0053aa64cbf2808aaf6f4eaee30281adc979bdee6eb2 31 | certs/saal23-2.p12:41ff31ce10ffa17f1efdf57fa106296d3f3e3c966a22ce1116e8cc05309b4c75 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:buster 2 | 3 | RUN apt-get -y update && \ 4 | apt-get install -y libopus-dev portaudio19-dev pulseaudio && \ 5 | apt-get clean && \ 6 | rm -rf /var/lib/apt/lists/* 7 | 8 | COPY requirements.txt /tmp 9 | RUN pip install -r /tmp/requirements.txt && \ 10 | rm -f /tmp/requirements.txt 11 | 12 | RUN useradd -ms /bin/bash app 13 | USER app 14 | WORKDIR /home/app 15 | COPY --chown=app c3lingo_mumble ./c3lingo_mumble 16 | COPY --chown=app examples ./examples 17 | 18 | 19 | ENTRYPOINT ["/usr/local/bin/python"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Philip Stark 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | pymumble = {editable = true,git = "https://github.com/azlux/pymumble.git"} 10 | opuslib = "*" 11 | google = "*" 12 | protobuf = "*" 13 | pyyaml = "*" 14 | pyaudio = "*" 15 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7621d3cb18e628d37d4d83b7ec42bb7e0d823d99d49dad584ce7245113a77a38" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "beautifulsoup4": { 18 | "hashes": [ 19 | "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da", 20 | "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a" 21 | ], 22 | "markers": "python_full_version >= '3.6.0'", 23 | "version": "==4.12.2" 24 | }, 25 | "google": { 26 | "hashes": [ 27 | "sha256:143530122ee5130509ad5e989f0512f7cb218b2d4eddbafbad40fd10e8d8ccbe", 28 | "sha256:889cf695f84e4ae2c55fbc0cfdaf4c1e729417fa52ab1db0485202ba173e4935" 29 | ], 30 | "index": "pypi", 31 | "version": "==3.0.0" 32 | }, 33 | "opuslib": { 34 | "hashes": [ 35 | "sha256:2cb045e5b03e7fc50dfefe431e3404dddddbd8f5961c10c51e32dfb69a044c97" 36 | ], 37 | "index": "pypi", 38 | "version": "==3.0.1" 39 | }, 40 | "protobuf": { 41 | "hashes": [ 42 | "sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7", 43 | "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c", 44 | "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2", 45 | "sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b", 46 | "sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050", 47 | "sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9", 48 | "sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7", 49 | "sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454", 50 | "sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480", 51 | "sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469", 52 | "sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c", 53 | "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e", 54 | "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db", 55 | "sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905", 56 | "sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b", 57 | "sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86", 58 | "sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4", 59 | "sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402", 60 | "sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7", 61 | "sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4", 62 | "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99", 63 | "sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee" 64 | ], 65 | "index": "pypi", 66 | "markers": "python_version >= '3.7'", 67 | "version": "==3.20.3" 68 | }, 69 | "pyaudio": { 70 | "hashes": [ 71 | "sha256:009f357ee5aa6bc8eb19d69921cd30e98c42cddd34210615d592a71d09c4bd57", 72 | "sha256:126065b5e82a1c03ba16e7c0404d8f54e17368836e7d2d92427358ad44fefe61", 73 | "sha256:12f2f1ba04e06ff95d80700a78967897a489c05e093e3bffa05a84ed9c0a7fa3", 74 | "sha256:2a166fc88d435a2779810dd2678354adc33499e9d4d7f937f28b20cc55893e83", 75 | "sha256:2dac0d6d675fe7e181ba88f2de88d321059b69abd52e3f4934a8878e03a7a074", 76 | "sha256:506b32a595f8693811682ab4b127602d404df7dfc453b499c91a80d0f7bad289", 77 | "sha256:5fce4bcdd2e0e8c063d835dbe2860dac46437506af509353c7f8114d4bacbd5b", 78 | "sha256:78dfff3879b4994d1f4fc6485646a57755c6ee3c19647a491f790a0895bd2f87", 79 | "sha256:858caf35b05c26d8fc62f1efa2e8f53d5fa1a01164842bd622f70ddc41f55000", 80 | "sha256:bbeb01d36a2f472ae5ee5e1451cacc42112986abe622f735bb870a5db77cf903", 81 | "sha256:f745109634a7c19fa4d6b8b7d6967c3123d988c9ade0cd35d4295ee1acdb53e9" 82 | ], 83 | "index": "pypi", 84 | "version": "==0.2.14" 85 | }, 86 | "pymumble": { 87 | "editable": true, 88 | "git": "https://github.com/azlux/pymumble.git", 89 | "markers": "python_version >= '3.6'", 90 | "ref": "8be2d18ac7324669e1fcd9b37083560fadb9e9e7" 91 | }, 92 | "pyyaml": { 93 | "hashes": [ 94 | "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", 95 | "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", 96 | "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", 97 | "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", 98 | "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", 99 | "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", 100 | "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", 101 | "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", 102 | "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", 103 | "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", 104 | "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", 105 | "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", 106 | "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", 107 | "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", 108 | "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", 109 | "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", 110 | "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", 111 | "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", 112 | "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", 113 | "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", 114 | "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", 115 | "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", 116 | "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", 117 | "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", 118 | "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", 119 | "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", 120 | "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", 121 | "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", 122 | "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", 123 | "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", 124 | "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", 125 | "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", 126 | "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", 127 | "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", 128 | "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", 129 | "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", 130 | "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", 131 | "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", 132 | "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", 133 | "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", 134 | "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", 135 | "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", 136 | "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", 137 | "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", 138 | "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", 139 | "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", 140 | "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", 141 | "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", 142 | "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", 143 | "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" 144 | ], 145 | "index": "pypi", 146 | "markers": "python_version >= '3.6'", 147 | "version": "==6.0.1" 148 | }, 149 | "soupsieve": { 150 | "hashes": [ 151 | "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", 152 | "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" 153 | ], 154 | "markers": "python_version >= '3.8'", 155 | "version": "==2.5" 156 | } 157 | }, 158 | "develop": {} 159 | } 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Docker Image CI](https://github.com/c3lingo/c3lingo-mumble/workflows/Docker%20Image%20CI/badge.svg) 2 | 3 | # Mumble Audio Utilities 4 | 5 | This project contains Python modules that are used in conjunction with a Mumble server to insert and extract audio into/from Mumble channels. 6 | 7 | ## Installation 8 | 9 | The Python code requires Python 3.7 or newer, and `pipenv` or `venv` and `pip`. 10 | 11 | ### Docker 12 | 13 | A docker image is available at https://hub.docker.com/r/c3lingo/c3lingo-mumble. Use the docker image like you would use the Python command: 14 | 15 | ``` 16 | docker run --rm -it -v $PWD/c3lingo-mumbleweb:/c3lingo-mumbleweb c3lingo/c3lingo-mumble:latest -m c3lingo_mumble.play_wav -c /c3lingo-mumbleweb/test-channel.yaml 17 | ``` 18 | 19 | ### Local Installation 20 | 21 | Install the prerequisite packages, then set up a local environment. 22 | 23 | #### Debian 24 | ``` 25 | sudo apt install -y git python3-dev python3-venv python3-wheel libopus-dev portaudio19-dev pulseaudio 26 | ``` 27 | 28 | #### Mac 29 | ```sh 30 | brew install opus python portaudio 31 | ``` 32 | 33 | #### Local Environment with Pipenv 34 | 35 | With `pipenv`: 36 | ``` 37 | $ pipenv install 38 | ``` 39 | 40 | #### Local Environment with Pip 41 | 42 | With `venv` and `pip` 43 | ``` 44 | $ python3 -m venv .venv 45 | $ .venv/bin/pip install -r requirements.txt 46 | ``` 47 | 48 | ## Using c3lingo-mumble 49 | 50 | ### Playing a WAV file to a Mumble channel with `play_wav` 51 | 52 | This module connects to a channel and plays the wav file. The file can be played in a loop. 53 | 54 | ``` 55 | python -m c3lingo_mumble.play_wav -c examples/play_wav/test-channel.yaml 56 | ``` 57 | 58 | The config file contains all necessary information. See [examples/play_wav/test-channel.yaml](./examples/play_wav/test-channel.yaml) for an example. 59 | 60 | 61 | ### Receive audio from a channel with `recv_stdout` and send it to stdout 62 | 63 | This module connects to a channel and produces any audio received on standard out, as raw little endian 16 bit PCM 48000 samples/sec. 64 | 65 | ``` 66 | python -m c3lingo_mumble.recv_stdout mumble.c3lingo.org test 67 | ``` 68 | 69 | The optional third argument specifies a file to record to. Recording stops when the Python program is stopped (^C or kill). 70 | 71 | 72 | ### Receive audio from a channel with `recv_pyaudio` and send it to an output 73 | 74 | This module connects to a channel and send the sound to a [PortAudio](http://www.portaudio.com) device. 75 | 76 | ``` 77 | python -m c3lingo_mumble.recv_stdout mumble.c3lingo.org test headphones 78 | ``` 79 | 80 | The third argument specifies the device to play the audio on. To get a list of devices, run 81 | ``` 82 | python -m c3lingo_mumble.audio 83 | ``` 84 | Then use the index or the name of the device. 85 | 86 | ### Additional Modules 87 | 88 | There are more useful modules. See the source code and the (examples/)[examples/] directory for more information. 89 | 90 | ## Building 91 | 92 | ### Updating `requirements.txt` 93 | 94 | ``` 95 | pipenv run pip freeze > requirements.txt 96 | ``` 97 | 98 | ## Setup at 36c3 99 | 100 | Audio is taken directly from the Voctomix setup (on the Voctomix host). 101 | 102 | We connect to Voctomix to receive a stream consisting of a Matroska container 103 | with both video and audio, and use ffmpeg to extract the audio channels. 104 | 105 | ``` 106 | $ ffprobe tcp://localhost:15000 107 | ffprobe version 4.1.4-1~deb10u1 Copyright (c) 2007-2019 the FFmpeg developers 108 | ... 109 | Input #0, matroska,webm, from 'tcp://localhost:15000': 110 | Metadata: 111 | encoder : GStreamer matroskamux version 1.14.4 112 | creation_time : 2019-12-11T17:06:50.000000Z 113 | Duration: N/A, start: 49.920000, bitrate: 6144 kb/s 114 | Stream #0:0(eng): Video: ... 115 | Metadata: 116 | title : Video 117 | Stream #0:1(eng): Audio: pcm_s16le, 48000 Hz, 8 channels, s16, 6144 kb/s (default) 118 | Metadata: 119 | title : Audio 120 | 121 | ``` 122 | 123 | There are four stereo audio channels interleaved into a single stream. We use 124 | ffmpeg to extract and downmix the audio to three mono channels: for the original 125 | sound from the speaker, one for the first translation channel, and one for 126 | the second one. The fourth source channel is ignored. 127 | 128 | We then feed this into the Python code that sends each channel to the configured 129 | Mumble channel, using a unique user for that channel. 130 | 131 | The pipeline looks like this: 132 | ``` 133 | ffmpeg -loglevel error -i tcp://localhost:15000 \ 134 | -filter_complex '[0:a]pan=3c|c0=.5*c0+.5*c1|c1=.5*c2+.5*c3|c2=.5*c4+.5*c5[a0]' \ 135 | -map '[a0]' -ac 3 -f s16le -c:a pcm_s16le -y - \ 136 | | pipenv run python -m c3lingo_mumble.send_stdin -f c3lingo-adams.yaml 137 | ``` 138 | 139 | A sample config file for the Python code can be found in 140 | [examples/send_stdin/adams.yaml](examples/send_stdin/adams.yaml). 141 | 142 | A sample systemd unit file can be found under 143 | [examples/send_stdin/mumblesender.service](examples/send_stdin/mumblesender.service) 144 | -------------------------------------------------------------------------------- /c3lingo_mumble/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c3lingo/c3lingo-mumble/20d5b8f1751c0c609d9d47341eb4b3804aef29dd/c3lingo_mumble/__init__.py -------------------------------------------------------------------------------- /c3lingo_mumble/asio_singleton.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import sounddevice 3 | 4 | import sys 5 | import logging 6 | 7 | LOG = logging.getLogger("bar") 8 | 9 | class AsioSingleton(object): 10 | 11 | def __init__(self): 12 | self.listeners = {} 13 | self.number_of_channels = 0 14 | self.audio_input_thread = None 15 | self.initialized = False 16 | 17 | def initialize(self, audio_input): 18 | if self.initialized: 19 | return 20 | 21 | self.number_of_channels = sounddevice.query_devices(audio_input)["max_input_channels"] 22 | 23 | self.audio_input_thread = sounddevice.InputStream( 24 | samplerate=48000, # PyMumble wills it. 25 | device=audio_input, 26 | channels=self.number_of_channels, 27 | callback=self.inputstream_callback, 28 | blocksize=32, # TODO: Move to configuration 29 | dtype='int16') 30 | self.audio_input_thread.start() 31 | self.initialized = True 32 | 33 | def add_listener(self, channel, listener): 34 | self.listeners[channel] = listener 35 | 36 | def inputstream_callback(self, indata: np.ndarray, frames: int, time, status: sounddevice.CallbackFlags): 37 | """This is called (from a separate thread) for each audio block.""" 38 | 39 | # if status: 40 | # print(status, file=sys.stderr) 41 | 42 | for channel, mumble_client in self.listeners.items(): 43 | 44 | if mumble_client.mumble_ready: 45 | mumble_client.mumble_conn_thread.sound_output.add_sound(indata[:, channel - 1].tobytes()) 46 | # else: 47 | # print('.', file=sys.stderr, end='') 48 | del indata 49 | 50 | 51 | input_stream = AsioSingleton() 52 | -------------------------------------------------------------------------------- /c3lingo_mumble/audio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Helper to find the desired PortAudio device. 5 | """ 6 | 7 | import os 8 | import re 9 | import sys 10 | import wave 11 | 12 | import pyaudio 13 | 14 | from c3lingo_mumble.stdchannel_redirected import stdchannel_redirected 15 | 16 | class Audio: 17 | def __init__(self): 18 | with stdchannel_redirected(sys.stderr, os.devnull): 19 | self.pyaudio = pyaudio.PyAudio() 20 | 21 | def get_devinfo(self, regex, min_input_channels=1, min_output_channels=1): 22 | if regex.isdigit(): 23 | return self.pyaudio.get_device_info_by_host_api_device_index(0, int(regex)) 24 | regex = re.compile(regex, re.IGNORECASE) 25 | info = self.pyaudio.get_host_api_info_by_index(0) 26 | numdevices = info.get('deviceCount') 27 | for i in range (0, numdevices): 28 | devinfo = self.pyaudio.get_device_info_by_host_api_device_index(0, i) 29 | if regex.match(devinfo['name']) \ 30 | and devinfo['maxInputChannels'] >= min_input_channels \ 31 | and devinfo['maxOutputChannels'] >= min_output_channels: 32 | return devinfo 33 | return None 34 | 35 | def list_devices(self): 36 | info = self.pyaudio.get_host_api_info_by_index(0) 37 | numdevices = info.get('deviceCount') 38 | print(f'{numdevices} PyAudio devices: (input/output channels)') 39 | for i in range (0, numdevices): 40 | devinfo = self.pyaudio.get_device_info_by_host_api_device_index(0, i) 41 | print(f' {i} - "{devinfo["name"]}" ({devinfo["maxInputChannels"]}/{devinfo["maxOutputChannels"]})') 42 | 43 | if __name__ == "__main__": 44 | a = Audio() 45 | a.list_devices() -------------------------------------------------------------------------------- /c3lingo_mumble/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import sys 5 | 6 | import yaml 7 | 8 | 9 | class Config: 10 | def __init__(self, defaults={}, description=None): 11 | self.config = {} 12 | self.defaults = defaults 13 | self.description = description 14 | 15 | def add_config_args(self, d): 16 | for (k, v) in d.items(): 17 | if isinstance(v, dict): 18 | self.add_config_args(v) 19 | else: 20 | self.parser.add_argument('--{}'.format(k)) 21 | 22 | def update_config(self, config, args): 23 | for (k, v) in config.items(): 24 | if isinstance(v, dict): 25 | self.update_config(v, args) 26 | elif k in args and args[k] is not None: 27 | config[k] = args[k] 28 | 29 | def load_yaml(self, file): 30 | with open(file, 'r') as fd: 31 | y = yaml.safe_load(fd) 32 | self.update_config(self.config, y) 33 | 34 | def get_config(self, args=sys.argv[1:]): 35 | self.parser = argparse.ArgumentParser(description=self.description) 36 | self.parser.add_argument('-c', '--config') 37 | self.add_config_args(self.defaults) 38 | c = self.parser.parse_args(args) 39 | self.config = dict(self.defaults) 40 | if c.config: 41 | self.load_yaml(c.config) 42 | self.update_config(self.config, dict(vars(c).items())) 43 | return self.config 44 | -------------------------------------------------------------------------------- /c3lingo_mumble/config_loader.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def load_config(config_path): 5 | 6 | with open(config_path, 'r') as config_json: 7 | return json.load(config_json) -------------------------------------------------------------------------------- /c3lingo_mumble/manylisteners.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Create many mumble clients listening to a channel. This can be used as a load 5 | test tool. 6 | 7 | usage: python -m c3lingo_mumble.manylisteners localhost client-a 300 100 60 8 | """ 9 | import random 10 | import sys 11 | import time 12 | 13 | from c3lingo_mumble.config import Config 14 | 15 | from pymumble_py3 import Mumble 16 | from pymumble_py3.callbacks import PYMUMBLE_CLBK_SOUNDRECEIVED 17 | from pymumble_py3.constants import PYMUMBLE_CONN_STATE_NOT_CONNECTED 18 | 19 | 20 | class MumbleListener: 21 | def __init__(self, server, nick, channel, debug=False): 22 | self.nick = nick.format(channel=channel) 23 | self.mumble = Mumble(server, self.nick, password='somepassword', debug=debug) 24 | self.mumble.set_application_string('Audio Meter for Channel {}'.format(channel)) 25 | self.mumble.callbacks.set_callback(PYMUMBLE_CLBK_SOUNDRECEIVED, self.sound_received_handler) 26 | self.mumble.set_receive_sound(1) 27 | self.mumble.start() 28 | self.mumble.is_ready() 29 | if channel is not 'root': 30 | self.channel = self.mumble.channels.find_by_name(channel) 31 | self.channel.move_in() 32 | 33 | def sound_received_handler(self, user, sound): 34 | pass 35 | 36 | 37 | def get_channel_list(server): 38 | mumble = Mumble(server, 'random_user', password='somepassword', debug=False) 39 | mumble.start() 40 | mumble.is_ready() 41 | channels = [c['name'] for c in list(mumble.channels.values())[1:]] 42 | if mumble.is_alive(): 43 | mumble.connected = PYMUMBLE_CONN_STATE_NOT_CONNECTED 44 | mumble.control_socket.close() 45 | return channels 46 | 47 | 48 | if __name__ == "__main__": 49 | if len(sys.argv) != 6: 50 | print('usage: manylisteners server basename ramp_seconds count sustain_seconds') 51 | sys.exit(64) 52 | (server, basename, ramp_seconds, count, sustain_seconds) = sys.argv[1:] 53 | channels = get_channel_list(server) 54 | print(channels) 55 | listeners = [] 56 | 57 | for i in range(0, int(count)): 58 | channel = random.choice(channels) 59 | nick = f'{basename}-{i:05d}' 60 | print(f'adding {nick}@{server}, channel {channel}') 61 | listeners.append(MumbleListener(server, nick, channel)) 62 | time.sleep(float(ramp_seconds) / int(count)) 63 | print(f'ramp to {count} listeners completed, sleeping for {sustain_seconds} seconds') 64 | time.sleep(float(sustain_seconds)) 65 | print(f'Done.') 66 | sys.exit(0) -------------------------------------------------------------------------------- /c3lingo_mumble/mapping.py: -------------------------------------------------------------------------------- 1 | 2 | class ConfigurationError(Exception): 3 | pass 4 | 5 | class MumbleMapping(object): 6 | 7 | def __init__(self, audio_channel, mumble_username, mumble_channel, mumble_cert, mumble_key): 8 | self.mumble_cert = self.check_cert(mumble_cert) 9 | self.mumble_key = self.check_cert(mumble_key) 10 | self.mumble_channel = self.check_mumble_channel(mumble_channel) 11 | self.mumble_username = self.check_mumble_username(mumble_username) 12 | self.audio_channel = self.check_audio_channel(audio_channel) 13 | 14 | @staticmethod 15 | def check_cert(mumble_cert): 16 | if isinstance(mumble_cert, str): 17 | if os.path.isfile(mumble_cert): 18 | return mumble_cert 19 | else: 20 | raise ConfigurationError("Could not find Mumble Certificate at {}.".format(mumble_cert)) 21 | else: 22 | raise ConfigurationError("Mumble Certificate is not an instance of str.") 23 | 24 | @staticmethod 25 | def check_mumble_channel(mumble_channel): 26 | if isinstance(mumble_channel, str): 27 | return mumble_channel 28 | else: 29 | raise ConfigurationError("Mumble channel is not an instance of str.") 30 | 31 | @staticmethod 32 | def check_mumble_username(mumble_username): 33 | if isinstance(mumble_username, str): 34 | return mumble_username 35 | else: 36 | raise ConfigurationError("Mumble User Name is not an instance of str.") 37 | 38 | @staticmethod 39 | def check_audio_channel(audio_channel): 40 | try: 41 | audio_channel_int = int(audio_channel) 42 | if audio_channel_int > 0: 43 | return audio_channel_int 44 | else: 45 | raise ConfigurationError("Audio Channel is 1-indexed. Zero or negative values are not valid.") 46 | except ValueError as e: 47 | raise ConfigurationError("Audio Channel could not be converted to an int.") 48 | -------------------------------------------------------------------------------- /c3lingo_mumble/mumble_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains a class to stream audio from an Audio input to a Mumble 3 | channel. 4 | """ 5 | import os 6 | import sys 7 | import time 8 | import queue 9 | import logging 10 | import threading 11 | import pymumble_py3 as pymumble 12 | from queue import Empty 13 | from threading import Thread 14 | from pymumble_py3.constants import PYMUMBLE_CONN_STATE_CONNECTED, PYMUMBLE_CONN_STATE_NOT_CONNECTED 15 | 16 | LOG = logging.getLogger(__name__) 17 | LOG.setLevel(logging.INFO) 18 | 19 | 20 | class MumbleClient(object): 21 | def start_listening(self): 22 | def thread_func(audio_queue, data_stream): 23 | while True: 24 | data = data_stream.read(128) 25 | audio_queue.put(data) 26 | 27 | listener_thread = Thread(target=thread_func, args=(self.audio_queue, self.stream)) 28 | listener_thread.start() 29 | return listener_thread 30 | 31 | def start_streaming(self): 32 | running = True 33 | 34 | def thread_func(audio_queue, mumble_obj): 35 | while True: 36 | try: 37 | while True: 38 | data = audio_queue.get(False) 39 | 40 | if self.mumble_ready: 41 | mumble_obj.sound_output.add_sound(data) 42 | else: 43 | del data 44 | 45 | except Empty: 46 | time.sleep(0.01) 47 | 48 | streamer_thread = Thread(target=thread_func, args=(self.audio_queue, self.mumble_conn_thread)) 49 | streamer_thread.start() 50 | return streamer_thread 51 | 52 | def __init__(self, hostname, port, channel, user, cert, key, stream): 53 | self.stream = stream 54 | self.mumble_channel = channel 55 | self.audio_queue = queue.Queue() 56 | self.thread_comm_queue = queue.Queue() 57 | 58 | self.mumble_conn_thread = pymumble.Mumble(host=hostname, 59 | port=port, 60 | user=user, 61 | certfile=cert, 62 | keyfile=key, 63 | debug=False, 64 | reconnect=True) 65 | 66 | self.mumble_conn_thread._set_ident() 67 | 68 | self.mumble_conn_thread.set_application_string("c3lingo (%s)" % 0.1) 69 | self.mumble_conn_thread.set_codec_profile('audio') 70 | 71 | @property 72 | def mumble_ready(self): 73 | # lock_acquired = self.mumble_conn_thread.ready_lock.acquire(False) 74 | # if lock_acquired: 75 | # self.mumble_conn_thread.ready_lock.release() 76 | mumble_connection_status = getattr(self.mumble_conn_thread, "connected", PYMUMBLE_CONN_STATE_NOT_CONNECTED) 77 | return mumble_connection_status == PYMUMBLE_CONN_STATE_CONNECTED 78 | 79 | def start(self): 80 | LOG.error("starting threads") 81 | 82 | self.mumble_conn_thread.start() 83 | LOG.error("mumble thread started") 84 | 85 | self.wait_for_mumble_ready() 86 | LOG.error("mumble ready") 87 | 88 | self.listener_thread = self.start_listening() 89 | self.streamer_thread = self.start_streaming() 90 | 91 | LOG.error("done starting threads") 92 | return self.thread_comm_queue 93 | 94 | def wait_for_mumble_ready(self): 95 | self.mumble_conn_thread.is_ready() 96 | 97 | self.mumble_conn_thread.set_bandwidth(64000) 98 | 99 | foo = (self.mumble_conn_thread.channels 100 | .find_by_name(self.mumble_channel)) 101 | foo.move_in(self.mumble_conn_thread.users.myself_session) 102 | -------------------------------------------------------------------------------- /c3lingo_mumble/play_wav.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import audioop 3 | import logging 4 | import math 5 | import sys 6 | import time 7 | import wave 8 | 9 | from c3lingo_mumble.config import Config 10 | import pymumble_py3 11 | 12 | 13 | def dBFS(value): 14 | if value < 1: 15 | value = 1 16 | return 20 * math.log10(value / 32767) + 3 17 | 18 | 19 | def volume(buffer): 20 | return dBFS(audioop.rms(buffer, 2)) 21 | 22 | 23 | def play_wav(server_args, channelname, file, level): 24 | mumble = pymumble_py3.Mumble(**server_args) 25 | mumble.set_receive_sound(True) 26 | mumble.start() 27 | mumble.is_ready() 28 | log = logging.getLogger('play_wav') 29 | 30 | if not mumble.is_alive(): 31 | raise Exception(f'Connection to "{server_args["host"]}" failed') 32 | channel = mumble.channels.find_by_name(channelname) 33 | channel.move_in() 34 | 35 | deadline = time.monotonic() + 2 36 | while time.monotonic() < deadline: 37 | if mumble.my_channel() == channel: 38 | break 39 | time.sleep(.1) 40 | if mumble.my_channel() != channel: 41 | raise Exception(f'Unable to move to channel "{channelname}"') 42 | 43 | try: 44 | while True: 45 | with wave.open(file, 'rb') as wf: 46 | duration = 1.0 * wf.getnframes() / wf.getframerate() 47 | chunk = int(wf.getframerate() * 0.1) # 100ms chunks 48 | start = time.perf_counter() 49 | data = wf.readframes(chunk) 50 | while len(data) > 0: 51 | v = volume(data) 52 | log.debug(f'Seconds to send: {mumble.sound_output.get_buffer_size():5.1f}s, volume: {v:5.1f} dbFS') 53 | if v > level: 54 | mumble.sound_output.add_sound(data) 55 | time.sleep(mumble.sound_output.get_buffer_size() * 0.9) 56 | else: 57 | log.debug(f'Skipping chunk because volume {v:5.1f} dBFS is below {level:3.0f} dBFS') 58 | data = wf.readframes(chunk) 59 | now = time.perf_counter() 60 | elapsed = now - start 61 | remaining = duration - elapsed + 1 62 | if remaining > 0: 63 | time.sleep(remaining) 64 | except KeyboardInterrupt: 65 | mumble.control_socket.close() 66 | 67 | 68 | if __name__ == "__main__": 69 | config = Config(description='Send a WAV file to a Mumble server', 70 | defaults={ 71 | 'file': None, 72 | 'mumble-server': { 73 | # see pymumble_py3.mumbly.Mumble.__init__ 74 | 'host': None, 75 | 'port': 64738, 76 | 'user': 'play_wav', 77 | 'password': '', 78 | 'certfile': None, 79 | 'keyfile': None, 80 | 'reconnect': False, 81 | 'tokens': [], 82 | 'debug': False, 83 | }, 84 | 'channel': None, 85 | 'loop': True, 86 | 'level': -999 87 | }) 88 | c = config.get_config() 89 | for k in ('file', 'channel'): 90 | if k not in c or c[k] is None: 91 | print('Missing required parameter --{}'.format(k)) 92 | sys.exit(64) 93 | for k in ('host',): 94 | if k not in c['mumble-server'] or c['mumble-server'][k] is None: 95 | print('Missing required parameter --{}'.format(k)) 96 | sys.exit(64) 97 | lh = logging.StreamHandler(stream=sys.stderr) 98 | lh.setLevel(logging.DEBUG if c['mumble-server']['debug'] else logging.INFO) 99 | lh.setFormatter(logging.Formatter('%(asctime)s-%(name)s-%(levelname)s-%(message)s')) 100 | logging.root.addHandler(lh) 101 | log = logging.getLogger('play_wav') 102 | log.setLevel(logging.INFO) 103 | log.info(f"Playing \"c['file']\" on channel \"c['channel']\" at c['mumble-server']['host']") 104 | try: 105 | play_wav(c['mumble-server'], c['channel'], c['file'], c['level']) 106 | except Exception as e: 107 | print(f'Unable to play file: {e.__class__.__name__}: {e}', file=sys.stderr) 108 | 109 | -------------------------------------------------------------------------------- /c3lingo_mumble/program_source.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import numpy as np 4 | 5 | LOG = logging.getLogger("ProgramSource") 6 | 7 | class BadConfigurationError(Exception): 8 | pass 9 | 10 | # class Program(object): 11 | # @classmethod 12 | # def from_dict(cls, dict_object): 13 | # try: 14 | # return cls(dict_object["invocation_string"], 15 | # dict_object["number_of_channels"]) 16 | # except: 17 | # raise BadConfigurationError() 18 | 19 | # def __init__(self, invocation_string, number_of_channels): 20 | # self.invocation_string = invocation_string 21 | # self.number_of_channels = number_of_channels 22 | 23 | class ProgramSource(object): 24 | 25 | def __init__(self): 26 | self.initialized = False 27 | 28 | def initialize(self, invocation_string, channels): 29 | if self.initialized: 30 | return 31 | 32 | 33 | # self.number_of_channels = sounddevice.query_devices(audio_input)["max_input_channels"] 34 | 35 | # self.audio_input_thread = sounddevice.InputStream( 36 | # samplerate=48000, # PyMumble wills it. 37 | # device=audio_input, 38 | # channels=self.number_of_channels, 39 | # callback=self.inputstream_callback, 40 | # blocksize=32, # TODO: Move to configuration 41 | # dtype='int16') 42 | # self.audio_input_thread.start() 43 | self.initialized = True 44 | 45 | def add_listener(self, channel, listener): 46 | self.listeners[channel] = listener 47 | 48 | def inputstream_callback(self, indata: np.ndarray, frames: int, time, status: sounddevice.CallbackFlags): 49 | """This is called (from a separate thread) for each audio block.""" 50 | 51 | # if status: 52 | # print(status, file=sys.stderr) 53 | 54 | for channel, mumble_client in self.listeners.items(): 55 | 56 | if mumble_client.mumble_ready: 57 | mumble_client.mumble_conn_thread.sound_output.add_sound(indata[:, channel - 1].tobytes()) 58 | # else: 59 | # print('.', file=sys.stderr, end='') 60 | del indata 61 | 62 | 63 | input_stream = AsioSingleton() 64 | -------------------------------------------------------------------------------- /c3lingo_mumble/recv_pyaudio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import random 5 | import string 6 | import sys 7 | import time 8 | import threading 9 | 10 | import pyaudio 11 | 12 | from array import array 13 | 14 | from pymumble_py3 import Mumble 15 | 16 | from c3lingo_mumble.audio import Audio 17 | 18 | 19 | class MumbleReceiver: 20 | def __init__(self, server, channel, dev, nick='recv-{r}@{channel}', use_cb=False, debug=False): 21 | r = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) 22 | self.channelname = channel 23 | self.nick = nick.format(r=r, channel=channel) 24 | self.mumble = Mumble(server, self.nick, password='somepassword', 25 | debug=debug) 26 | self.mumble.set_application_string( 27 | 'Receiver for Channel {}'.format(channel)) 28 | audio = Audio() 29 | devinfo = audio.get_devinfo(dev, min_input_channels=0) 30 | if not devinfo: 31 | print(f"Unable to find output device \"{dev}\".", file=sys.stderr) 32 | print(f"use \"python -m c3lingo_mumble.audio\" to get a list of devices", file=sys.stderr) 33 | sys.exit(1) 34 | 35 | self.rate = 48000 36 | self.interval = .02 # 20ms of samples 37 | 38 | self.mumble.set_receive_sound(1) 39 | self.mumble.start() 40 | self.mumble.is_ready() 41 | 42 | self.channel = self.mumble.channels.find_by_name(self.channelname) 43 | self.channel.move_in() 44 | 45 | self.thread = None 46 | if use_cb: 47 | callback = self.pyaudio_send 48 | else: 49 | callback = None 50 | self.stream = audio.pyaudio.open(format=audio.pyaudio.get_format_from_width(2), 51 | channels=1, 52 | rate=48000, 53 | output_device_index=devinfo['index'], 54 | output=True, 55 | stream_callback=callback) 56 | if use_cb: 57 | self.stream.start_stream() 58 | else: 59 | self.start() 60 | 61 | def start(self): 62 | self.thread = threading.Thread(target=self.send, daemon=True) 63 | self.thread.start() 64 | 65 | def clip(self, val): 66 | return -32768 if val < -32768 else 32767 if val > 32767 else val 67 | 68 | def get_audio(self): 69 | buffer = array("h", [0] * int(self.interval * self.rate)) 70 | for user in self.channel.get_users(): 71 | samples = array("h") 72 | while len(samples) < len(buffer): 73 | sound = user.sound.get_sound(self.interval) 74 | if not sound: 75 | break 76 | samples.frombytes(sound.pcm) 77 | if sys.byteorder == 'big': 78 | samples.byteswap() 79 | for i in range(0, len(samples)): 80 | buffer[i] = self.clip(buffer[i] + samples[i]) 81 | if sys.byteorder == 'big': 82 | samples.byteswap() 83 | for i in range(0, len(samples)): 84 | buffer[i] = self.clip(buffer[i] + samples[i]) 85 | if sys.byteorder == 'big': 86 | buffer.byteswap() 87 | return buffer.tobytes() 88 | 89 | def send(self): 90 | ts = time.time() - self.interval 91 | while True: 92 | start = time.time() 93 | self.stream.write(self.get_audio()) 94 | wait = self.interval * 0.9 - (time.time() - start) 95 | if wait > 0: 96 | time.sleep(wait) 97 | while time.time() < ts: # spin until time is reached 98 | pass 99 | 100 | def pyaudio_send(self, in_data, frame_count, time_info, status): 101 | data = self.get_audio() 102 | return (data, pyaudio.paContinue) 103 | 104 | 105 | def main(): 106 | if len(sys.argv) < 3: 107 | print(f"usage: {os.path.basename(sys.argv[0])} server channel portaudio-device", file=sys.stderr) 108 | sys.exit(64) 109 | (ignore, server, channel, dev) = sys.argv 110 | client = MumbleReceiver(server, channel, dev) 111 | while True: 112 | time.sleep(1) 113 | 114 | 115 | if __name__ == "__main__": 116 | main() 117 | -------------------------------------------------------------------------------- /c3lingo_mumble/recv_stdout.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import random 5 | import string 6 | import sys 7 | import time 8 | import threading 9 | 10 | from array import array 11 | 12 | from pymumble_py3 import Mumble 13 | 14 | 15 | class MumbleReceiver: 16 | def __init__(self, server, channel, file, nick='recv-{r}@{channel}', debug=False): 17 | r = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) 18 | self.channelname = channel 19 | self.nick = nick.format(r=r, channel=channel) 20 | self.mumble = Mumble(server, self.nick, password='somepassword', 21 | debug=debug) 22 | self.mumble.set_application_string( 23 | 'Receiver for Channel {}'.format(channel)) 24 | if file: 25 | self.fd = open(file, "wb", 0o644); 26 | else: 27 | self.fd = os.fdopen(sys.stdout.fileno(), "wb", closefd=False) 28 | 29 | self.rate = 48000 30 | self.interval = .02 # 20ms of samples 31 | 32 | self.mumble.set_receive_sound(1) 33 | self.mumble.start() 34 | self.mumble.is_ready() 35 | 36 | self.channel = self.mumble.channels.find_by_name(self.channelname) 37 | self.channel.move_in() 38 | 39 | self.thread = None 40 | self.start() 41 | 42 | def start(self): 43 | self.thread = threading.Thread(target=self.send, daemon=True) 44 | self.thread.start() 45 | 46 | def clip(self, val): 47 | return -32768 if val < -32768 else 32767 if val > 32767 else val 48 | 49 | def send(self): 50 | ts = time.time() - self.interval 51 | while True: 52 | start = time.time() 53 | buffer = array("h", [0]*int(self.interval*self.rate)) 54 | for user in self.channel.get_users(): 55 | samples = array("h") 56 | while len(samples) < len(buffer): 57 | sound = user.sound.get_sound(self.interval) 58 | if not sound: 59 | # print(f"not enough samples: {len(samples)} < {len(buffer)}", file=sys.stderr) 60 | break 61 | samples.frombytes(sound.pcm) 62 | if sys.byteorder == 'big': 63 | samples.byteswap() 64 | for i in range(0, len(samples)): 65 | buffer[i] = self.clip(buffer[i] + samples[i]) 66 | if sys.byteorder == 'big': 67 | buffer.byteswap() 68 | self.fd.write(buffer.tobytes()) 69 | wait = self.interval * 0.9 - (time.time() - start) 70 | if wait > 0: 71 | time.sleep(wait) 72 | while time.time() < ts: # spin until time is reached 73 | pass 74 | 75 | 76 | def main(): 77 | if len(sys.argv) < 3: 78 | print(f"usage: {os.path.basename(sys.argv[0])} server channel [file]", file=sys.stderr) 79 | sys.exit(64) 80 | (ignore, server, channel, *file) = sys.argv 81 | if file and len(file) > 0: 82 | file = file[0] 83 | client = MumbleReceiver(server, channel, file) 84 | while True: 85 | time.sleep(1) 86 | 87 | 88 | if __name__ == "__main__": 89 | main() 90 | -------------------------------------------------------------------------------- /c3lingo_mumble/register.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Connect to server and register username and certificate. 5 | """ 6 | import sys 7 | import time 8 | 9 | from c3lingo_mumble.config import Config 10 | import pymumble_py3 11 | 12 | 13 | def register(server_args, user, certfile, keyfile): 14 | mumble = pymumble_py3.Mumble(**server_args) 15 | mumble.start() 16 | mumble.is_ready() 17 | 18 | if not mumble.is_alive(): 19 | raise Exception(f'Connection to "{server_args["host"]}" failed') 20 | 21 | server_args['certfile'] = certfile 22 | server_args['keyfile'] = keyfile 23 | server_args['user'] = user 24 | admin = pymumble_py3.Mumble(**server_args) 25 | admin.start() 26 | admin.is_ready() 27 | if not admin.is_alive(): 28 | raise Exception(f'Connection to "{server_args["host"]}" as "{user}" failed') 29 | 30 | admin.users[mumble.users.myself_session].register() 31 | admin.channels.new_channel(0, mumble.user, False) 32 | time.sleep(1) 33 | 34 | print(f'User {mumble.user} registered') 35 | 36 | if __name__ == "__main__": 37 | config = Config(description='Register a user cert with the server', 38 | defaults={ 39 | 'mumble-server': { 40 | # see pymumble_py3.mumbly.Mumble.__init__ 41 | 'host': None, 42 | 'port': 64738, 43 | 'user': None, 44 | 'password': '', 45 | 'certfile': None, 46 | 'keyfile': None, 47 | 'reconnect': False, 48 | 'tokens': [], 49 | 'debug': False, 50 | }, 51 | 'admin': None, 52 | 'admin_certfile': None, 53 | 'admin_keyfile': None, 54 | }) 55 | c = config.get_config() 56 | for k in ('admin', 'admin_certfile', 'admin_keyfile'): 57 | if k not in c or not c[k]: 58 | print('Missing required parameter --{}'.format(k)) 59 | sys.exit(64) 60 | for k in ('host', 'user'): 61 | if k not in c['mumble-server'] or not c['mumble-server'][k]: 62 | print('Missing required parameter --{}'.format(k)) 63 | sys.exit(64) 64 | print(f'Registering user \"{c["mumble-server"]["user"]}\" with cert \"{c["mumble-server"]["certfile"]}\"') 65 | try: 66 | register(c['mumble-server'], c['admin'], c['admin_certfile'], c['admin_keyfile']) 67 | except Exception as e: 68 | print(f'Unable to register user: {e.__class__.__name__}: {e}', file=sys.stderr) 69 | -------------------------------------------------------------------------------- /c3lingo_mumble/send_pyaudio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Send one or more channels of audio recording from PyAudio sources to 5 | selected Mumble channels. 6 | """ 7 | import argparse 8 | import struct 9 | import sys 10 | import threading 11 | import time 12 | 13 | import pymumble_py3 14 | import yaml 15 | 16 | from c3lingo_mumble.audio import Audio 17 | 18 | 19 | class AppError(Exception): 20 | pass 21 | 22 | 23 | class MumbleSender: 24 | def __init__(self, server_args, channelname): 25 | self.server_args = server_args 26 | self.channel = channelname 27 | self.count = 0 28 | 29 | mumble = pymumble_py3.Mumble(**server_args) 30 | self.mumble = mumble 31 | mumble.start() 32 | mumble.is_ready() 33 | 34 | if not mumble.is_alive(): 35 | raise AppError(f'Connection to "{server_args["host"]}" failed') 36 | channel = mumble.channels.find_by_name(channelname) 37 | channel.move_in() 38 | 39 | deadline = time.monotonic() + 2 40 | while time.monotonic() < deadline: 41 | if mumble.my_channel() == channel: 42 | break 43 | time.sleep(.1) 44 | if mumble.my_channel() != channel: 45 | raise AppError(f'Unable to move to channel "{channelname}"') 46 | 47 | def send(self, data): 48 | self.mumble.sound_output.add_sound(data) 49 | self.count += 1 50 | 51 | 52 | class PyAudioSender: 53 | def __init__(self, name, config, audio): 54 | self.name = name 55 | self.config = config 56 | self.audio = audio 57 | self.mumbles = {} 58 | self.devinfo = audio.get_devinfo(source) 59 | if not self.devinfo: 60 | raise AppError(f'Unable to find device matching "{source}"') 61 | self.maxchannels = self.devinfo['maxInputChannels'] 62 | for (index, params) in config.items(): 63 | if index > self.devinfo['maxInputChannels'] - 1: 64 | raise AppError( 65 | f'Channel index {index} is too large for device "{self.devinfo["name"]}"') 66 | self.mumbles[index] = MumbleSender(params['server'], params['channel']) 67 | print(f'Connected {self.devinfo["name"]}/{index} to {params["server"]["host"]}/{params["channel"]}') 68 | 69 | def start(self): 70 | self.thread = threading.Thread(target=self.send, daemon=True) 71 | self.thread.start() 72 | 73 | @staticmethod 74 | def split_channels(multi, nchannels): 75 | singles = [] 76 | for i in range(0, nchannels): 77 | singles.append(b'') 78 | for samples in struct.iter_unpack(f'<{nchannels}h', multi): 79 | for i in range(0, nchannels): 80 | singles[i] += struct.pack('', file=sys.stderr) 107 | sys.exit(64) 108 | try: 109 | with open(c.file, 'r') as file: 110 | config = yaml.safe_load(file) 111 | audio = Audio() 112 | inputs = [] 113 | for (source, params) in config['sources'].items(): 114 | inputs.append(PyAudioSender(source, params, audio)) 115 | for input in inputs: 116 | input.start() 117 | while True: 118 | # counters = [] 119 | # for mumble in input.mumbles.values(): 120 | # counters.append(mumble.count) 121 | # print(f' {counters}') 122 | time.sleep(1) 123 | except AppError as e: 124 | print(f'Unable to send audio: {e.__class__.__name__}: {e}', file=sys.stderr) 125 | -------------------------------------------------------------------------------- /c3lingo_mumble/send_stdin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Send one or more channels of audio recording from PyAudio sources to 5 | selected Mumble channels. 6 | """ 7 | import argparse 8 | import audioop 9 | import math 10 | import os 11 | import re 12 | import struct 13 | import sys 14 | import threading 15 | import time 16 | import wave 17 | 18 | import pymumble_py3 19 | import yaml 20 | 21 | from c3lingo_mumble.stdchannel_redirected import stdchannel_redirected 22 | 23 | 24 | class AppError(Exception): 25 | pass 26 | 27 | 28 | def dBFS(value): 29 | if value < 1: 30 | value = 1 31 | return 20 * math.log10(value / 32767) + 3 32 | 33 | 34 | def volume(buffer): 35 | return dBFS(audioop.rms(buffer, 2)) 36 | 37 | 38 | class MumbleSender: 39 | def __init__(self, server_args, channelname, level=-999): 40 | self.server_args = server_args 41 | self.channel = channelname 42 | self.level = level 43 | self.count = 0 44 | self.last_report = time.time() 45 | self.report_interval = 30 # report every 30 seconds 46 | self.report_lag_above = 2 # more than 2 seconds lag 47 | 48 | mumble = pymumble_py3.Mumble(**server_args) 49 | self.mumble = mumble 50 | mumble.set_receive_sound(True) 51 | mumble.start() 52 | mumble.is_ready() 53 | 54 | if not mumble.is_alive(): 55 | raise AppError(f'Connection to "{server_args["host"]}" failed') 56 | channel = mumble.channels.find_by_name(channelname) 57 | channel.move_in() 58 | 59 | deadline = time.monotonic() + 2 60 | while time.monotonic() < deadline: 61 | if mumble.my_channel() == channel: 62 | break 63 | time.sleep(.1) 64 | if mumble.my_channel() != channel: 65 | raise AppError(f'Unable to move to channel "{channelname}"') 66 | 67 | def send(self, data): 68 | v = volume(data) 69 | if self.mumble.sound_output is not None and v > self.level: 70 | self.mumble.sound_output.add_sound(data) 71 | self.count += 1 72 | if time.time() > self.last_report + self.report_interval: 73 | if self.mumble.sound_output.get_buffer_size() > self.report_lag_above: 74 | print(f"warning: {self.channel} has buffered {self.mumble.sound_output.get_buffer_size():5.1f}s of audio") 75 | self.last_report = time.time() 76 | 77 | 78 | class StdinSender: 79 | def __init__(self, config): 80 | self.config = config 81 | self.mumbles = {} 82 | self.maxchannels = len(config) 83 | for (index, params) in enumerate(self.config): 84 | level = params['level'] if 'level' in params else -999 85 | self.mumbles[index] = MumbleSender(params['server'], params['channel'], level) 86 | print(f'Connected {index} to {params["server"]["host"]}/{params["channel"]}, minimum level {level}') 87 | 88 | def start(self): 89 | self.thread = threading.Thread(target=self.send, daemon=True) 90 | self.thread.start() 91 | 92 | @staticmethod 93 | def split_channels(multi, nchannels): 94 | singles = [] 95 | for i in range(0, nchannels): 96 | singles.append(b'') 97 | for samples in struct.iter_unpack(f'<{nchannels}h', multi): 98 | for i in range(0, nchannels): 99 | singles[i] += struct.pack('', file=sys.stderr) 121 | sys.exit(64) 122 | try: 123 | with open(c.file, 'r') as file: 124 | config = yaml.safe_load(file) 125 | if 'channels' not in config: 126 | print('The config file must specify a channel mapping under "channels"') 127 | sys.exit(64) 128 | input = StdinSender(config['channels']) 129 | input.start() 130 | input.thread.join() 131 | except AppError as e: 132 | print(f'Unable to send audio: {e.__class__.__name__}: {e}', file=sys.stderr) 133 | -------------------------------------------------------------------------------- /c3lingo_mumble/single_wrapper.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import sys 4 | import os 5 | from queue import Empty 6 | 7 | from c3lingo_mumble.mumble_client import MumbleClient 8 | 9 | host = os.environ['HOST'] 10 | port = os.environ['PORT'] 11 | channel = os.environ['MUMBLE_CHANNEL'] 12 | user = os.environ['MUMBLE_USER'] 13 | cert = os.environ['MUMBLE_CERT'] 14 | key = os.environ['MUMBLE_KEY'] 15 | 16 | 17 | def handle_event(event, instance): 18 | if event.type == "Disconnected" or "Stopped": 19 | instance.run() 20 | instance.retries += 1 21 | 22 | 23 | def main(): 24 | 25 | instance = MumbleClient(host, int(port), channel, user, cert, key, sys.stdin.buffer) 26 | 27 | instance.retries = 0 28 | 29 | # something happens! 30 | instance_queue = instance.start() 31 | 32 | while True: 33 | try: 34 | handle_event(instance_queue.get(False), instance) 35 | except Empty: 36 | time.sleep(0.01) 37 | 38 | if __name__ == "__main__": 39 | main() -------------------------------------------------------------------------------- /c3lingo_mumble/stdchannel_redirected.py: -------------------------------------------------------------------------------- 1 | """ 2 | Temporarily redirect stdout, stderr or stdin to some other file. 3 | 4 | http://marc-abramowitz.com/archives/2013/07/19/python-context-manager-for-redirected-stdout-and-stderr/ 5 | 6 | e.g.: 7 | 8 | with stdchannel_redirected(sys.stderr, os.devnull): 9 | ... 10 | """ 11 | import contextlib 12 | import os 13 | 14 | 15 | @contextlib.contextmanager 16 | def stdchannel_redirected(stdchannel, dest_filename): 17 | oldstdchannel = None 18 | dest_file = None 19 | try: 20 | oldstdchannel = os.dup(stdchannel.fileno()) 21 | dest_file = open(dest_filename, 'w') 22 | os.dup2(dest_file.fileno(), stdchannel.fileno()) 23 | 24 | yield 25 | finally: 26 | if oldstdchannel is not None: 27 | os.dup2(oldstdchannel, stdchannel.fileno()) 28 | if dest_file is not None: 29 | dest_file.close() 30 | -------------------------------------------------------------------------------- /c3lingo_mumble/wrapper.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from queue import Empty 5 | from . import config_loader 6 | from .mumble_client import MumbleClient 7 | from . import asio_singleton 8 | 9 | input_stream = asio_singleton.input_stream 10 | 11 | 12 | def handle_event(event, instance): 13 | if event.type == "Disconnected" or "Stopped": 14 | instance.run() 15 | instance.retries += 1 16 | 17 | 18 | def main(config_path): 19 | 20 | config = config_loader.load_config(config_path) 21 | 22 | instances = [] 23 | retries = [] 24 | queues = [] 25 | 26 | server = config["server"] 27 | mappings = config["mappings"] 28 | 29 | # for x in range(0, 7): 30 | for mapping in mappings: 31 | instance = MumbleClient( 32 | server["hostname"], 33 | server["port"], 34 | mapping, 35 | input_stream) 36 | 37 | # setattr(instance, "retries", 0) 38 | instance.retries = 0 39 | instances.append(instance) 40 | 41 | for instance in instances: 42 | # something happens! 43 | instance_queue = instance.start() 44 | queues.append((instance_queue, instance)) 45 | 46 | while True: 47 | for queue, instance in queues: 48 | try: 49 | handle_event(queue.get(False), instance) 50 | except Empty: 51 | time.sleep(0.01) 52 | 53 | if __name__ == "__main__": 54 | main("config.json") -------------------------------------------------------------------------------- /certs/Saal_1-0-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOzCCAyOgAwIBAgIUKHGGcsrZyzzJQ8mROQrwyi+bz5AwDQYJKoZIhvcNAQEL 3 | BQAwRjEQMA4GA1UECgwHYzNsaW5nbzERMA8GA1UEAwwIU2FhbF8xLTAxHzAdBgkq 4 | hkiG9w0BCQEWEGluZm9AYzNsaW5nby5vcmcwHhcNMjMxMjIxMjMwODQ3WhcNMjQx 5 | MjIwMjMwODQ3WjBGMRAwDgYDVQQKDAdjM2xpbmdvMREwDwYDVQQDDAhTYWFsXzEt 6 | MDEfMB0GCSqGSIb3DQEJARYQaW5mb0BjM2xpbmdvLm9yZzCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBAN9eYr0rcBBmP0wKocbJhZiD/R8ImKmv0Q1ExvPD 8 | O2c02c2Dq0eNhjRg0lEaRFrpyZnQz24KZW2f8YE+eX0VjiMuVgwm3TslsoziMjKI 9 | DkF15Sls2Le0hMBln6xBVr2V4YiLIejMgUwgZbwnt1MkSfrYWg2DDm1aN71msZer 10 | K2LqF4eHzb5YWHMrwbG7LKM+PfiHTO8daiGXmD539PqI50k3M5P5tw43pXtGV4tJ 11 | oirQ9vmNpSienR2BF7w1XeV8ta6u0wERbnqcz6CuX8mnnJqaUjxCofdib2DsFGmx 12 | 8OMpIpTuq8NpagLESOcaxE2qqp3QENrX3wr7txl9OuvZW9U6eLSm0oEo8wvpxZTj 13 | aiphcx8PCa1knOnGUKANPkCPTminamB46aYgwkfX5P6P+fqRI0AnwO1ALlnAycPO 14 | R0ETpF/ua5HREUTsdA0/shfzWru6kXAPFpMeURuRhDBvnzg+EDiv1LKJ8Cbi83Zu 15 | LUZDjIXRrTtZjjGDh1J8WhzV9B9p8GrKweUD/Yn0ujyc5YakSzVUrfaKAfmWtWr/ 16 | c9LJQr0efNtdVrFtYuzu0IPXEb6CLVifxht8Iy43+4KiIq+lhReP2ZxEVorIjcQf 17 | oREgAUEnZ4UTFv37TD2t9Q0a5Ci6qxLzYBT+i0t0kzZqC+O1MwbcvMswaPoVZ5mT 18 | 9s+bAgMBAAGjITAfMB0GA1UdDgQWBBRGmGglpRtJbzDoTszcB+mByw99LTANBgkq 19 | hkiG9w0BAQsFAAOCAgEAPB7J9+cJNWuI+SBgFvCXcggK6mhtUBPhcCC3L8a9tody 20 | /De01h+8eiqnfSOh2uAvUtcjudzYHaQx8WvSAG/NZW4olov6Q7b/8oroj91dZRW7 21 | WloIJKPSPS0j1MBb1CkZC2BoSgzFxfkdbwWrnH+V6nWyp8jTd02hCYU8L3bIZXc9 22 | VeoMsoQK6ML6/Aa0f9Ak+0eBaCc+E+HQMp3gB50i/pKGPoFaPLaXQuWQqVGVyb5v 23 | pfluR7ywW9WPPeKh/0mcQm5I6quIyOPMBoZ1fyDbDZpX1OsADdaRruIsp+NBucKZ 24 | x0B2GnLT6A7Hz80vMaT1UQvV/v3W8505mm5n1HiL8oS3Gh4APx+fynuBr6AgeJgw 25 | RaaVWm9Pnh9xCokvcTic3lwcYvOnUaCCpFw0MZYLr5u5G/6w+GL3xjLYpihy8WI6 26 | 7z+jYErP/zoRfreu4GdxF94ayUYVdoNzHa+ral6SJQOf7GEu029gEcMq89CPJQYS 27 | aNMDa4WFYBQowpyAlsp6AO4vSJcGnljN8oJQm6VTLAZW/sNr/MVSV/G0rk/TgjFL 28 | tVtH59Dy+236GgkWep6ePvGzLX9YMVzrzRbhpuOz2cAYBhNHu58ZSbP2LxsS9Xbu 29 | zg5ly+xS2Go4fiis5OVU9QuqN9FYQw1lOKL97d4UYGqVpKTUdL45YKsE7nqeMT8= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/Saal_1-1-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOzCCAyOgAwIBAgIUaKFPXm9Q+9fx7+quJZoHehK8SakwDQYJKoZIhvcNAQEL 3 | BQAwRjEQMA4GA1UECgwHYzNsaW5nbzERMA8GA1UEAwwIU2FhbF8xLTExHzAdBgkq 4 | hkiG9w0BCQEWEGluZm9AYzNsaW5nby5vcmcwHhcNMjMxMjIxMjMwODQ4WhcNMjQx 5 | MjIwMjMwODQ4WjBGMRAwDgYDVQQKDAdjM2xpbmdvMREwDwYDVQQDDAhTYWFsXzEt 6 | MTEfMB0GCSqGSIb3DQEJARYQaW5mb0BjM2xpbmdvLm9yZzCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBALW7HaWr2dec6eHw2voLaqlJAt7Y/9ZhUpiPphsO 8 | FIJpMOElNpLhcTn6w5QLLLUh8U6MzD5dUVufnjI52fwfcfvLzVl2SDLDkWaZFpaH 9 | C3axCAIEnIsvqKVYM4gjkLIrr/5AiQdM1OppaNP3Dw5zoBGRXWV10q/2PRg+YUjb 10 | o92U+eScSNBAHX+K2oJS23IqBOA0RsPMyoXZa/Mtxp8Q+iEPVY7fO2qFzCI+Mgh2 11 | w1i4hagdLvqPlrgwxp6tAxBdeJQXVWnTOvvrlLAlWaBrBgEdUjoogKnKP9UR7/mI 12 | 1n2secesQFSCLsGbdukAmm3xZtw3rnX9m7BGvyD29xZlAPk4jYF1WMIIqP3Fm8oW 13 | jltBcEWIgjao+MVSST69O4f8ywlPWuOZA9zES9nSiNPps46zsL8lnB2gas3hUpol 14 | h66bu9bOSFg8U5moo3Lc5gF2m6UpFSHB2xJE+qnTrwApQ5YajN29wmIF44HMU59X 15 | sqLM36DDO0POOdFNgORPCipR1/w5cVxStGFJIcD7nqU4s9xhuZ+67Rqawithcc7f 16 | TahGVWwoq3u63lrE1036P+E9qZHPiXciMG9u3iVvZozrgcRruQeoe92OcaoMpzvz 17 | Y+mrCwqU7i1H28EENLJ7lzcgmEEsXNdkLVSGED9lMYs2HIPROX9xzrPMuzJU3i/k 18 | Zx9ZAgMBAAGjITAfMB0GA1UdDgQWBBTqovbrUlMcHhkGDxnwvgrbw9urMzANBgkq 19 | hkiG9w0BAQsFAAOCAgEAUi+YBnVEYIiQGgbiyO/min4kCBGfCBglMsJ+rmBXoX9I 20 | 9KPUINMMKYc/XLGBy8FGK9xbMTM03x6xVmz06DuyB9XjeQNfdBnNNtByGyvBXoSA 21 | xfHtQj1N+Fjn7KMIgnYr4wj5MWQ0BVWhVWDkeVDVjD7bYkDzYoHDEf/m5lWTHmDu 22 | g3Xt8N/YNp8KMwEj2PtZR6dNcyjdobkqnctzIPIoCOneBjT3/ugoaot8U507LDEt 23 | 3gN4HyawxB1MdaDEupVW+jepCcXAEC1Mx4fw+inrP2nCw4G2FV46ERFsQpGO1DHj 24 | 1wE+FZoz3HEMsSZdsCCpB810YOuw8uT1reiGoXOykwo+p/7R7QnDNhbjEpzOEFVK 25 | tF5AmNTaHCMhcZcqp8VRnJ+Ee0pcXAM/xhZxMV/Hab2t6xkGr+oP6p4BwQ5qjbiG 26 | PAPMpLg61A/RkVZ1hQiigfWB1FAZxX3HGOM/CVJYYCOvtl4SgCWoJqPN0IYt0DpD 27 | 4tac5IgcEoKNtcLrMC9SwIvWsakmLX3gymRIigCdDKqg+aN5DRXRTrBPWp805vvY 28 | 2KTo0Ez2Dng5gSF7Df5zQy9MmMAbTgL4WT+/aVJLNNPVY+rurVo/Dbo5GL7TY8s3 29 | 84f6XACT/5+3ozBR1zgPGaJPjc0cG1PRrRjaD5z53mfTG54QaAYuByyeMZae2WE= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/Saal_1-2-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOzCCAyOgAwIBAgIUOehuJ4XRxeE9RiwlFGiJPX8LGk8wDQYJKoZIhvcNAQEL 3 | BQAwRjEQMA4GA1UECgwHYzNsaW5nbzERMA8GA1UEAwwIU2FhbF8xLTIxHzAdBgkq 4 | hkiG9w0BCQEWEGluZm9AYzNsaW5nby5vcmcwHhcNMjMxMjIxMjMwODQ5WhcNMjQx 5 | MjIwMjMwODQ5WjBGMRAwDgYDVQQKDAdjM2xpbmdvMREwDwYDVQQDDAhTYWFsXzEt 6 | MjEfMB0GCSqGSIb3DQEJARYQaW5mb0BjM2xpbmdvLm9yZzCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBAMTVqbfGIidT4E8UPtHfxBvaj9DsDSXcyrFEzJwq 8 | vCeM+ciJkOBnTyXG9RWzaq8/Hi/bPzfzl+bnDNgH9yE6JqpMcYZOo0135oVNKvKR 9 | cmJ8/8Y1NkhEuuy3zxXUrQ/jbZCOkjz1cBnJOH2iz9aenqwdSlkPuX15P4RVkjWn 10 | eCN020OsK6LfoRePnmYJEmYTtQXml8Iz4ZGm/2nrUttvQUcjTm7nZSAGT4TR0JTn 11 | +vMmqIV3PaYZkd8a4YoHaL4bQJhYm2Ik9JuWM76epc8ipBZgsu8p7SIOve8Y3SH4 12 | 4HdkB70+oMjQWUPxdaLnFbi8tteVtMILmHuHdpNFyv66gzfGU3X+DoyhMPMW535C 13 | R+E3HMF50EVeObbDYgE0n1EHnDuYQeIcHHE5tVPaOlZ/s4ALJp1VRStWnCeGSd5v 14 | OjsDo3JLbJeWW6VoS2PCp7/MEEG2b5EXQ2hIdjO6GkkDvfkjnbld19RoZG3wyBOx 15 | Jv+LwiT3OyMipYu963rgJolRmhUHsRD2Ec8+pWunpUSbdhnVJxGeSlTNxYM8JfSm 16 | O4AevDy3ZHGHJKRpTgQsRao6QYFxF2gf0y3gOGVvV2BS4uOCVgM3vfZeTI5hpESv 17 | QRt2d0ewY+RMiAlD7OyWL4cr6VDxecHBeonJupRQnakKQABpWnVeY08tUwtVKB69 18 | hsLNAgMBAAGjITAfMB0GA1UdDgQWBBR3zk5kvalQyB2wNPnREFBMG3CraDANBgkq 19 | hkiG9w0BAQsFAAOCAgEAuw2+qVtRu31saXst4XHS9BCA3lUWjIzduLlZpvbzdLQS 20 | vQXmz6o/6WAnut7KjHI4FuTNglDmNZ8ZHZq9hZ3qHHWhxNSyasaF8QaDBAaKLqmV 21 | F5L6jKLG2dwtAiC+9LBjMeyZTb5j4rZggB45Fqr+r8Wg3fpULV0sR/AyKTLxnT8o 22 | xhBM+A7n9tBK54+/jHnV3rItNweS9FkO6ieP+lAQteKfLHCVQP+tkJmgDg1Puw6X 23 | PUqqPF96IIFUUgvXzJEtWrPF90vtTUEZKqEUMvbvbqgJHP733HEeDDHKAz0NBoXt 24 | MRtQ79m+gTbuxfuGVk33OpMnh29Jnk9HauLukrAePSQerACQyHkOssJt+ruOAgkS 25 | OLGb1NzXrgEejDl74wwbZA/L/QlVeL4AS5tDaehIT4MKLZyZGLpDGO/++KInEYZF 26 | Y8qCqPj8/5skrtemeGgQtHwwqIVKydYcO2Z0fTlT6zPXUPehOLmBVxPjuAxJ3Ey+ 27 | DIWq0uayhIWxvSzBpRKJen4KeE+jp2AmXtxFAwxGIePRhGlRNhUaeUoUAnfh1NDn 28 | VEDeParmrw1927Q80vkMNa2WRuXuM8bJa5qIb+3iRck4cKo0M14j/6m+amwHqRJ0 29 | M/TVgunezCier36dRoBD8ZRp3UIVg6Zx4/se2orA8FnRjA7AhmvQfkXSW0EBwns= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/Saal_G-0-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOzCCAyOgAwIBAgIUcAkToBw74H57169G/mVJOWo2SFEwDQYJKoZIhvcNAQEL 3 | BQAwRjEQMA4GA1UECgwHYzNsaW5nbzERMA8GA1UEAwwIU2FhbF9HLTAxHzAdBgkq 4 | hkiG9w0BCQEWEGluZm9AYzNsaW5nby5vcmcwHhcNMjMxMjIxMjMwODQ5WhcNMjQx 5 | MjIwMjMwODQ5WjBGMRAwDgYDVQQKDAdjM2xpbmdvMREwDwYDVQQDDAhTYWFsX0ct 6 | MDEfMB0GCSqGSIb3DQEJARYQaW5mb0BjM2xpbmdvLm9yZzCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBALXEShUb7rfX2CjK3uPjRUPx+3Ld7l4PK/AjNlsg 8 | 1WWZ6gHjyd1tg1FVJWlWYZqd8xGwhR8PK5z1QSNMK3+btNwqR1W4Kvg44iemXZsP 9 | WTpnkeHWZpPSH+XpW+kxKr1n8v6Te/tMZ5YSQq+51X3A+ei5ZTymAZpHTVy1Gdaz 10 | w0BtV3cWNLzN/HE5sT1bmKGzce7w2hCapkUOytzpMNn44x3LCNkHs/13U21uupb5 11 | Z874IPL1VTY2uEkkzyJxykhAkyYCPsqPbByh47YkZNvDlYxDfCPkmDMiKRVcErRh 12 | NS6d5jffwxP4SChjJ0Id+akSUnJ5LDrbEPUJnHLZXkgjOPgJf0b5JymU1plYlVht 13 | GB4fdE74I0/LUQkM73jAy6X6AQ/7TN0zhwZssw1PatwfWwA1kMC3KjQhI2m1rQaZ 14 | cuMN3oCTf8lvvUyBNtcMAPRYXl0oQIFrtWRFitoFQOINfHPjV6ZkjIdFzMn4Bjyu 15 | s+d7bXs4IjmcBWydlN8Es7RQZI8MK0QxwmBdTbs+SJyXMbmqJWJ8zLjHXI1Rpt3o 16 | ERghrr9osvTtQPMwQ7pKonzNWh85oDucnYIJYwDNYuedvKdYsouyTM3K+1cNZzgu 17 | infYvfGffxjMe+VV+By3aoyPglGqn4A3flcjvcy3jvXGwQIrB2GvwXJT1WLgWssW 18 | lufdAgMBAAGjITAfMB0GA1UdDgQWBBTYeQ0uT4VTxquEDLGVYvWob5nSQDANBgkq 19 | hkiG9w0BAQsFAAOCAgEAO0uePcdrQO0WH8UfYanAErQ8oYp+1C4eWh1/dHZ9vnrJ 20 | SYMhHu+w8oGyRcZoWWbAw6p9u9ogA5SQ+lJs6JZ5oG/2Db89irqfMAOKMrM63N5g 21 | Llgqul30v1jLk3zGH2DT9Vp+XsuGNFX5XPsbD2mu6aPLaRhwua5ts8iRS7UOq9mp 22 | rpvSePzBtB22bRhOAsnHkGPXMtQy+yPvAlZ/FHQK6OutlPhY2kqVm0aseTIGgwVD 23 | 8+4/F7X3DeSy0xj/DvY8vRzRl7EaFm7BvcfqCYzMBYHhRryuBWM0PViA+pKwpeeG 24 | QUuxyvxyTAPzEznbbC0TTEaqmzbmRwwDAs78t+V4J4TTJnpuV58Gi4MlquErmH8G 25 | Vve763s2BCEjU76AVWx7RzNFXYrU645/jT7Uf7STf1JmNDZyIDcSQ34EcuPYkeSS 26 | WjkSdQd1agQOL3nMbuYdKOMLogGZcZTYhZdxi+1UWBckc1EzqnoZrF2CP19Vnx+I 27 | aDdqJvtm16jOiOFmbGqCtiuJ0b6hULvJS5k6qPrHReRl1Rln8BKXDX0UGuF3JfPu 28 | 1iTboCbdMDdyPgRe33L7DMzbW5UPf/7G/b+QmfgMFmGvADJUKHChLRdA7X9iRIYO 29 | 25dQ5Js+etpX6XT66wNm0fH6ZOWPc4U2run5Cd734DLZr5E7sFRMwBFMRYUXo68= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/Saal_G-1-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOzCCAyOgAwIBAgIUTohLYs9x+dKBqAJORtuGAOAXuqowDQYJKoZIhvcNAQEL 3 | BQAwRjEQMA4GA1UECgwHYzNsaW5nbzERMA8GA1UEAwwIU2FhbF9HLTExHzAdBgkq 4 | hkiG9w0BCQEWEGluZm9AYzNsaW5nby5vcmcwHhcNMjMxMjIxMjMwODUwWhcNMjQx 5 | MjIwMjMwODUwWjBGMRAwDgYDVQQKDAdjM2xpbmdvMREwDwYDVQQDDAhTYWFsX0ct 6 | MTEfMB0GCSqGSIb3DQEJARYQaW5mb0BjM2xpbmdvLm9yZzCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBAKy/hGgXFihfQLgh1p0DZIkkFmiurAx7xuJR3uQ5 8 | Lw+V6IOXRdKvqS9rKVOqi4BAUjfgCUR7Ppgcrhs3BDgPU77tvIdIBhNNWjpZLapH 9 | MQAJ5e9z/WOYtiGh2vE1c8ncUKYz9IWbzG8hT61/mwdvEN9Qa0eml0SLHe8ESoIn 10 | zCiuHBXqLbfZBQfEJmc5Adi7VAMYxzYELhIG3WKkRW6ElgioXVwwpMQU9kJAKEpQ 11 | aWlQMAHR01X1M3D0aOF6yWSYAfAHB7bZSj+kiaHmWlhIIwaBChxrHPkI6QB4wigz 12 | FEX9b95rzhVKCUPbiYurqJoXldlv/hkIBgljPR9d6K6wAo1NXnu5EEZ3Cuv//CFY 13 | 2ls8eGg3atOaXYP+1Z5oOwKmw4bPWI4Sk8YQ5I63JOJgfF4a0Z/820TttZFQ17Zd 14 | 00pZTU0tct8KOguVNlzYh3E45ZjlNZ4BvHgK+cinpzsywCUoz1z11gSIOM8eQPui 15 | HlqLUH/6ceJQNbiNbL8DHsBlmyYlvAhhKzwETMZq5WaB243WNFkILiV0KitNcsbt 16 | 0rXoUOBJa2JeZWb2qzF/hJ7AiW2zusp6u2aNf0aZNaGwaLB3H4R6zFajFrxuDE91 17 | ChvvmJYRbVuyP7d+eG0BPKziCSiE3uuAMxb0o90Yrf5TGh+1rYOfYD3ZhFUmsjrG 18 | Ua27AgMBAAGjITAfMB0GA1UdDgQWBBQeUtBWUYCZ/iIWOuL2PZG/rWdC2TANBgkq 19 | hkiG9w0BAQsFAAOCAgEAIQrghyhOIvxYqftTjlF/+vk/HFbeuZNpdQfVit04F9P4 20 | k5h3o7MAA/2KgNfSSDGvOr6TDo8m0ip978vpPhcEqsiJkOu6uD/7G9q5U9p+QZsn 21 | qgZIs6xv8x9LaNFtRFZDP3Hi0iSRUrjx2NcS2BAoDgq5pyPIw3ytY03wIU2PUhsw 22 | uymQ6CGCTyhUofZlHx5KLyjVScyAOPhdxDqQVRbixKtCvrcjf1/hPX+6ZxRxtde0 23 | CfLhU5qtPz67yeLncgW+tZ30WfxPcK0ipUvz31iqnHOj5fQaFHuW/TH0pHh9IHmQ 24 | 1Bu3EDk3RRrdbb2sxTiN56TORoJK4NZIhWIvkY7uYOgZohnjvWiAQJcVnP3924S6 25 | 6FvmehogiM9eRErM4Y47gha5Wdpd5tZjb2Jl0Kt89PH19wgpf8HNyyNcjrpaZyOj 26 | AeTJSYe7Tw0Q0qBc3hDGXanI7JCjiQsln3GFUILm/RxGWrMdNJzkNZcrced6FicT 27 | 4EIHdj+SP8HMVk76ObauJ93ep4pc+gGimw/uzF+f857G1sJ6nUVByEMrWQo7uQCL 28 | 24PnVaffdekOunek2SmaB+YeNp5s1LlGPzC07/utjLwM8PIalit1JWCLztsfRq+y 29 | RcjPBRvchTDMBdUHOUcZXpGT/OfdvzwiL5NNHANJgdbeJQbpew/p6bpKR04aSOg= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/Saal_G-2-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOzCCAyOgAwIBAgIUTCQkKax6JfVyX9ZvwyQCc0gBZoYwDQYJKoZIhvcNAQEL 3 | BQAwRjEQMA4GA1UECgwHYzNsaW5nbzERMA8GA1UEAwwIU2FhbF9HLTIxHzAdBgkq 4 | hkiG9w0BCQEWEGluZm9AYzNsaW5nby5vcmcwHhcNMjMxMjIxMjMwODUwWhcNMjQx 5 | MjIwMjMwODUwWjBGMRAwDgYDVQQKDAdjM2xpbmdvMREwDwYDVQQDDAhTYWFsX0ct 6 | MjEfMB0GCSqGSIb3DQEJARYQaW5mb0BjM2xpbmdvLm9yZzCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBALdfH05zOlLfxD95bB5ke3K8eQ2ycFgs2iiD7X7M 8 | xxsGI4yDpTNs/Z4IfINikgG+Y7wJiyctU5setTGVFknrX3fv1uC13bj4a1kjWOl8 9 | 19FSh9FXxJDayLwnGSfB1r+XxmFnVk5l2qkCcfdgj/5raIGHdFjYV06z100TZdGE 10 | pLh0Htn9MuopLaflJa9L3Rexs3bOmItLtQF4zrmuxSbx7qyAry9dx7jskL1VhR4f 11 | FLHoFwjR33q0JSvzvLQYCf5r+LioxoxsEUOvVbt67X6o7qtZX7HkLGGyKFSR5Q8E 12 | f+ksUmPZMaz0pMRbT3XCig47GBO6OIV1Yv2GUu7YJ3NKyNNjsb/DpuR317UjMuAt 13 | JeyHT5RWM3+b+2Yf68wgzwtcOGXxrO+xk6LhQ/lyHh9wKnrlvApAm6auDs/fYTJc 14 | qQVjrSNUeMCKW+YDfC+EZi3uJWWU2X0x2UAG9u8sAJYTzutfdaAQ1h0xrd4Tbl3K 15 | RaC1Opur5vf2p/TdZdxEub3C2lCbob79E2UcMsK4QD9eVbHcczk0EdLf7iYPVihz 16 | FM/8a0//lV8GXtKEr5EexV4b24n9FeKL/BKl2SyF9qHBWQXju/gfJ4X8DDZs3MP6 17 | Pdm/9SRujoB4vRqhYu3vNsw2KSaxgRow4DAryr/P4x9TEVLqrqxWP7YYjNXeQqeS 18 | HD8BAgMBAAGjITAfMB0GA1UdDgQWBBRMH5ru78XlT9Q2BhXY45Y7lS2eKjANBgkq 19 | hkiG9w0BAQsFAAOCAgEARJMfJKNoO7oBiQImi53ACvmKEAnW/yf6FukwdhnbFyxA 20 | +oD/Yf1feItkuE+Jb7UlMaVlXd1CXIIImruvyrPRbJFB0zEnNSyme6dzrDFCMjda 21 | jr91Tyeh4Is/W9NIou8Ldk+RIyOXHXeGZjfCh2iw+zz7ZPygeYniXRTkUx54wRQ8 22 | yoHpmKZgweX/twO7AkfWX9W4jihfB/G1e/E7flwVnDr+XmPCOHMqYWGjcviOhhRq 23 | rvhxZH4i0G4haWXNVNHjpSyEmmxxez9kOrA97T8uz8fRcBiSl8sPXlMl4Lsyq0tx 24 | NJXntfE2wdEAmWx+g35BQsOYwfBLFv3ML88cO6bwoLQX8sPTIflaoUn8QL4QQr1C 25 | BoCWfLC9aD6oRTwap3ICn/rP5kY3u0HpAjDTV5ZGsrHSS7ffEGejmtaVwB0TA6FO 26 | CNL0G7ebaZ7951n8AWfnufA9/M7rzpp2gxZGMbETnzKqum8lzEads1ypn8epTw8Z 27 | KrnBLS06MNMa0uT6BEiCsbgeaH82qp7Kh5WYwNwC/q6pTna6tL0GcFL3peYih8YI 28 | 7oIpJftdA2tdS7ij0+SXmjZoj7BHoJqy9OI5FLQgpjvTMbj9iddHEwXjSbLlG0al 29 | Yp6Fl+D+yK4F8m5pdVsTjA83pt6I9YfsIAxWQe+8PlIe+xDmD8fvGEVyFswRFlw= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/Saal_Z-0-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOzCCAyOgAwIBAgIUGUfGQ9tDWc+WbSFtJ7JGHLP3p+IwDQYJKoZIhvcNAQEL 3 | BQAwRjEQMA4GA1UECgwHYzNsaW5nbzERMA8GA1UEAwwIU2FhbF9aLTAxHzAdBgkq 4 | hkiG9w0BCQEWEGluZm9AYzNsaW5nby5vcmcwHhcNMjMxMjIxMjMwODUxWhcNMjQx 5 | MjIwMjMwODUxWjBGMRAwDgYDVQQKDAdjM2xpbmdvMREwDwYDVQQDDAhTYWFsX1ot 6 | MDEfMB0GCSqGSIb3DQEJARYQaW5mb0BjM2xpbmdvLm9yZzCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBAKMRp9U7Gl7xE1K+UDJ7CZIpIYn8h1vkAtvksNvR 8 | hO5sbfLvQi2AU5DcScQdPMYB0/yOSDwkFUW4yMitEAICsQE/CQpuU9zAHqb1YkHG 9 | qXmMFZESuD2+UCKGiahDYEmTvHuf+to3uaVr9nRDuRrLI/ukgIKhLiURV7esOXfN 10 | xfWmDDwgDjZs6T36HeCOEP4YTPdLs/xcnfeHsWHJ5c2HyxUilwTGekCtvMh68Lbw 11 | 1jH9TP1hTLhhdo6ejbK+QCiqr3V7yR1nqMOMUtNPR8DiUXjgGMmbCl3mUNfMzczY 12 | zRrbe7mzUoQ9jp8IAyieK8jvfPg84CQhrJjM2BKORR6z7JATzvDaSwyHSlcLt19e 13 | 1/GLxSRSwlmUzUndqSYcZTUB/SAKcEVAAnXJF9AlAojjh7jOa8RXBi/n0G9tmKik 14 | 2l7YHQ/GMtG9/vuZASPK3t4i2rKCabh2rNm/OgNcFrhTFSNuu6DdsVVtWExSHrVE 15 | O4q1TEKp31bS1rdrqTbKdrY4caqlbr4PoFo3xsFVqZOlG3Bvmv+vveZ0BPEwoZXF 16 | YSEKL2EHJqzmFgphJNbuKXvgpsOCFLhotjjT+Dvohocyo6TNvh3AEjuoc3bLDEOz 17 | k3z93d7B0ixlZDZunzTD3XLJbNmw/dtmyEfHfVEzCpoPJKL5En+okappP4n/Y0HW 18 | cSqJAgMBAAGjITAfMB0GA1UdDgQWBBSfljj1PjbR0B5/nJTu2POKFA+olzANBgkq 19 | hkiG9w0BAQsFAAOCAgEAaVbh0r58U2lNPHV7Sxq9ezOoO9JSSLBSYWAGz5O6l8mV 20 | dYjyBgJ3xWgHoqH2BDw87hxSce0jP4uAMl/GaSFCnNE5/G22khBdCqDaMJM99KV7 21 | 6v13AJEUbl8TEM8GumSs9znam27lYczDPncIqW9FEYx+IiF0s087zKUsCcmNUCV8 22 | O57KXTctaJiwIK+4RFF1xm6k+5C3MYiwI4c3OCLhnmtsWZefE/8wpiQukT62QF1R 23 | Ur74cWiE2aK5ArzzKmNy/wgMYOrTLANxdi+aGRAHglJ9JC3u6QUfZp8VZp8Jxs2v 24 | JP1R9zyk8xVqJpCEo4fQ9epMLmYux7o0L1cSsICbs0gCdFvp1CJR0GJK3xi2S/5C 25 | 0ZuSGv66XIQ5VX3VE6qeq3RnCYZaQOSoZ6w2tY4a/scnOBJA8MIsHlLT8YTg80C1 26 | w7HWZBZsZPdZTcPGU/wacb+Tbe3QVmC9XlPPJ0sEK+u6gu7l+fF5SxW/uWeiTAJl 27 | q8k4u/EAvpe+35Rd7RmhVzduwo2q5aTbiTP93QA0lR6Age+ChH8Bglk6wIvobJhv 28 | Fol5SdGEETZhv4zRQzX6RIPpo6uhbsFcrKQC/FungQQuzy5Bhq2aO0r4VVaylmZc 29 | 6dQrwxXErli8q5aqXg09PeSEUwSQX2pFt1auWz8ei0oNj6PThwuHmHvwiEuNNz0= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/Saal_Z-1-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOzCCAyOgAwIBAgIUSaQ51Ykvu3sqy+k1FULvo1H6r5MwDQYJKoZIhvcNAQEL 3 | BQAwRjEQMA4GA1UECgwHYzNsaW5nbzERMA8GA1UEAwwIU2FhbF9aLTExHzAdBgkq 4 | hkiG9w0BCQEWEGluZm9AYzNsaW5nby5vcmcwHhcNMjMxMjIxMjMwODUyWhcNMjQx 5 | MjIwMjMwODUyWjBGMRAwDgYDVQQKDAdjM2xpbmdvMREwDwYDVQQDDAhTYWFsX1ot 6 | MTEfMB0GCSqGSIb3DQEJARYQaW5mb0BjM2xpbmdvLm9yZzCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBALbtRicBfhv6K3DUnHZkCxjcNOE4SEbGwtdr9bY2 8 | Zj1+A9Pig9PrtAXNxKY+jARlgi4t9gbUElOeHwIgvZ4YyzPhy0aCdpSy/YK3mP4O 9 | ZyPVeg8vjCl4a6plTAmF4QJfJJfcI8CsAUQvtlHXp1rm3UhsREzae0Ed6cRAmgJl 10 | BgcyfQroYAQECqNli7lKOvc+PMM78FEk0LMjFiW2aDOzlkwokCNFnNfSiSh9JofL 11 | 6CJcpj8nZKe4ANYGnvYcsobyx0Bl/u1Tx0qvI6TOziKEK/ijyKcTwPXj1ErUITer 12 | 2AarEeY/XsjmEJ3aTP80IA7/HRqzCmlNF6ExprSCq9BRU99RMGo/9Qc/hT1xGV5J 13 | na0R96xMZ9qMr1i7DMF3g+bp2GJ14a/7QjmeBtUXiRWhA5sU5Dg3bptqArOtVge+ 14 | +Up810flXxFiO5y2zG0RjKt57hXwrKfVSIwA/PM/aYVwFe0lNTOl2Q31WEdurqVK 15 | bUPfoONMu87sts8R+3/WohOGv5uWyy9dIg7pRyG5TAyhQAdv0egDWP4whsQQNpL4 16 | NJ90PNRPnj7M29bNbd7wa6Pc2rnYim13Rha4KLiHzMNB7NGsgjS5G62fjoCxxVWZ 17 | O4wpEKluXICLMmUNFj3f8pW0iPnUMZnKzUgxsFnCcc4+W5Us9t7IyOxUwEKGZ+wf 18 | etRXAgMBAAGjITAfMB0GA1UdDgQWBBRLqWyfP6/W6c4OaKn62lexT2ISHDANBgkq 19 | hkiG9w0BAQsFAAOCAgEAFjuyuANB3oaJEQnA1WOnaL/uX5CNFdudjMKs/tIl20BO 20 | H12n/yRgZfU+5BptsUE+SQ27cvhl6GQQ8Tu13zTE5ofglgf3jI2wDbirbXVz/CRW 21 | 7ysg+Afmas29kmH2hcrvtnebkoYMxbywiseSCMDNyk9n6uYDK5qSn7Yx7yFB4Ax0 22 | uGQa7YWX2uhXTvo/LwBhGyFoiVOS0KDxinMEqpkQdOQCzcEENw2w1qseZ7JhXp7M 23 | sMnykZk/g/VqRqA+kKs7MCD2LXvKkpJMqs2y5E1dGYpsg1JMoE9Jk/x41Qf4Pp+g 24 | yS2LKdUyZaDo1XFru4KO8ffRwjUj4SGkPX9mGdVrSLHchQwOcuUusr1Za/oF+5yI 25 | dgq2crLJSR1awTghkSmAjmZe8QKyHcpvCW3LQxX1ZqafYfNbaTSJEUsXmbLP4mKq 26 | v+ZautGhIYdgr1GUoRuPe2AKzJW8o9SV6uXS29+2s9IWQB8sv3orUdyXFRa4mYxY 27 | jiluhysJ7sa5h5ihT6i4eBaUWuZarfgez/0a5HcBtkcH01tPaoswqXvD02eee3FX 28 | L3WcI7PXdZ6V1tTuhjHamRTVlJom2dw50zlC+jXzJ02jnrTJDX7aMAcS1zlZcZBm 29 | KMmCrUTapMvyrgfe10sD1+ts+pqtFaVDcjQhGmD9lKIiIi75HcauzHBzAuepbJ8= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/Saal_Z-2-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOzCCAyOgAwIBAgIULzgURNzmBGxJOLzq+P6ZSPlCpbAwDQYJKoZIhvcNAQEL 3 | BQAwRjEQMA4GA1UECgwHYzNsaW5nbzERMA8GA1UEAwwIU2FhbF9aLTIxHzAdBgkq 4 | hkiG9w0BCQEWEGluZm9AYzNsaW5nby5vcmcwHhcNMjMxMjIxMjMwODUyWhcNMjQx 5 | MjIwMjMwODUyWjBGMRAwDgYDVQQKDAdjM2xpbmdvMREwDwYDVQQDDAhTYWFsX1ot 6 | MjEfMB0GCSqGSIb3DQEJARYQaW5mb0BjM2xpbmdvLm9yZzCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBAL8NXGbsuuojXyH0HFzl+P414HdKqpKFwHPjE1V/ 8 | 6O0nco1f26AZKBSeNVMrDZzHJrFurw2IdVVJZ2OhtOb0E7SAy//nJ3a+fXmk0mO1 9 | QurbNQ/aXkI08elI9Shg4hKCKBglkSvGStdnoB6Sbo1jFoNmg6MwscT3irrNBVFs 10 | q2iMzRGCcIuDIoSUDqiU7cCiGVxcBAPeeuH0DpTZZ+rLIozvH09bhUGOvYdwMZnG 11 | 0FW85kW7OsZjTp9ppZFjbcdm0iT8zpC95hmq+kK/E2jYipFsHc6NIzEEqAZpfe1g 12 | 0gA6WfvMCWapOP31rUBb8MHShVEXx7WaiXRuBagXity4uEbMfNp7Yo2ixXj2IsB7 13 | +I5yRG+D41kRbrHnOlwXJiYJ0VvTMVaiAenbtaN6Z+2K5gEVZCoZJ12c5IGu3gNo 14 | mU0V2S6A2QXAEURp3LprmN7TkSBauow9F/M8iDIsHszS0XK/KCN9Pkm9iAjKJ2tL 15 | sk+QEoP/+P4hEXrnxs419GIZU2shqP197XFVnMNoJjm7ntYNsU/dEfcqanKP3RId 16 | DhNC1SE+aehjOXYf3Y8pP8CCxwc3RRNO/n9sa8CPeeHSXInFdYWx94iuSxf1mq5V 17 | FyNMv9LnxZhGZ3vwd9odmT0APBvjSbQ3QLkmpHg35LEHYBlv2D06iDoutrN++Ymm 18 | 4+TnAgMBAAGjITAfMB0GA1UdDgQWBBRczLJSuRFGejqIisgXMDoXniO9RzANBgkq 19 | hkiG9w0BAQsFAAOCAgEAM99riSxhAUKYyX5QcDocj5NvruzDYbNd2odmLav1et7o 20 | dkxTPFvTCKCueX3ii9wqXx/iutVp8M/l4gU6JuUih7KUwVaPvQO8PzzcixzZVibM 21 | uLEtoY/94wX9HjbBQ2kU6QxjFjjBS/EMlCGj/j0dISCb3A73KH/QYl2ohqZzCA6/ 22 | xo443YrgdR7dg5h8SHmEcZA8VArCTjJGN25mZrgD2iGtyU527CstXkg8fJnZzTsI 23 | CDSP8JLL4Wnt8XMrCRlc2hUbAJfJ2YSKxvGxDSbOZHdZbZ5GKedYmxTUMJ5cEL1m 24 | gl1L8qK6/q5jE/OIBeK19Vs4nd9yXjI7+khMd8CPekeYfxJ4sCT+ctcl4vGPs+EW 25 | rpWHl8/eTvkOeDfo69YPA0wWj+JEnbfo7ZyD7dlmtZTTjiaUm/sKX1hxDQ/dGDo0 26 | QfiIfEY4g0FrN4EXniEZqnABLJzJb3B4rg65299NUB1xsdfzTUsgtex/WP4ygu/+ 27 | oN+io84qqOZgx6GmOmuyvS/ycE3LsT2ydmm1ITAAeXxGU47s3uajbZBDxLOaR08v 28 | h3PdiRm0+d7FCB38X9yWWjHfA9O2kRi4i8DaWR+qqmaUr+ljSBb47gMxb4GZ16Ts 29 | HP7utLL8i71qZIj2opJylB2x3OB3trJa7qYwGtUPTSnt37QEGmTtkI2bmzy8Cvk= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/stb-cert.pem: -------------------------------------------------------------------------------- 1 | Bag Attributes 2 | localKeyID: 0B D0 86 ED 38 83 D0 AD D3 5B 8B 93 44 97 28 E7 7E 58 E6 00 3 | friendlyName: Mumble Identity 4 | subject=/CN=stb 5 | issuer=/CN=stb 6 | -----BEGIN CERTIFICATE----- 7 | MIIDHDCCAgSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAOMQwwCgYDVQQDDANzdGIw 8 | HhcNMTkxMDI3MDkxMzQ4WhcNMzkxMDIyMDkxMzQ4WjAOMQwwCgYDVQQDDANzdGIw 9 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK5S6zlT4vY0JM9ZVrp5Tj 10 | OT3Yj1RlZ/Rhop03ThFxyz9tVQXa+PLzVBnnNOMeQmhL2vJYjpKRVImPLgYPatBb 11 | sKD0Jv8ApC/KbTYJDIujRvR449XgfUsX8hjtZMVJZIuvb1Iu/omlFx6YAlyQak49 12 | h3/JgntSNizolXK61+tnCuCtRNw9iK9NvDlsVq40uHIONzUPrT00sgla8Iu/m2m2 13 | /ecwGMxzvLFYb+gPh6UcQy1DaqDz/VFqeIopPrWuVWsNQoHy+G8mBTf8aozobtfN 14 | kgXCymBYSZOKnZhv8ooyx+AXztP6Iz1WOuOe/MpecPMk91H9UNPfQwFODZc/JvSh 15 | AgMBAAGjgYQwgYEwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDAjAd 16 | BgNVHQ4EFgQUGPn3zEk/cPsujrtjL5rpU/y/2vQwIgYJYIZIAYb4QgENBBUWE0dl 17 | bmVyYXRlZCBieSBNdW1ibGUwGQYDVR0RBBIwEIEOc3RiQGxhc3NpdHUuZGUwDQYJ 18 | KoZIhvcNAQEFBQADggEBACSC5qlVkZR8mTjgWvzI9BRhTFVWB0pDtf4RvYOcSXQt 19 | eTHh9gYlLg7+Nt31ZAaQ+OxkKD1qsBMe9Oz1WIPL7qLg+InH/UW9ubcLsv/o4S7d 20 | nUVRwnW68+PxCBSbYewzmFVMt6BmLq2Wm02Jx3PwnBkejgRl8CWWHmeRIXeLh52t 21 | 9X+LvwOXx/gjlmbr8WDLfUhZvwTcEo9lrnpTwcOmnfIiQNs2842mAlWH3Mqt6gkj 22 | sX1jorX8HThaJiD2AVwPgxloNyFNLXSBaQRIrjr26YW4f3XAL3P+Gnhlg5JVCVqQ 23 | n4OMljB/Qt5qgHOiLRB+gs87NRbJa25g3/WysIjMods= 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /certs/test-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFMzCCAxugAwIBAgIUb/5u8sU8vzBG+ZSre0696ohfIbswDQYJKoZIhvcNAQEL 3 | BQAwQjEQMA4GA1UECgwHYzNsaW5nbzENMAsGA1UEAwwEdGVzdDEfMB0GCSqGSIb3 4 | DQEJARYQaW5mb0BjM2xpbmdvLm9yZzAeFw0yMzEyMTUxNjAwNTlaFw0yNDEyMTQx 5 | NjAwNTlaMEIxEDAOBgNVBAoMB2MzbGluZ28xDTALBgNVBAMMBHRlc3QxHzAdBgkq 6 | hkiG9w0BCQEWEGluZm9AYzNsaW5nby5vcmcwggIiMA0GCSqGSIb3DQEBAQUAA4IC 7 | DwAwggIKAoICAQCbwyQTXLZGlSXRpGCs0qQiM0sceIKqbEDP3hAGtkCIIaKazTRs 8 | v+i6zBRQtL5yRZoPHnQNvoGYSrswTmQmTmZa8SJBZYJV7wyf+6ELWjslX2ZAxAVV 9 | mkGstgC2vdY2hl+SyXbOTg0aI6ZD4HWelYBHRNiO5cPgs8t9dAM6L4m1rwplYqri 10 | m5Ck34lGQT+E4pvDlSCt6XsOb/Rc5IgYGtq4/l44XGWpVjWU4s+eNo5vMA8QsNMu 11 | Jsj5segqXsgcCKaoaUBBB9rLiEMZJ7PeKIiNnz0V/650ZzwCKzCSmR76sCalzUex 12 | T4wYmwjOiV7v8yxhPmPfERjYJzsvOTcGMKkjtio5ybBBlthr5I2R7wkw4l+NcfWn 13 | yEtNzs3eRGMvw0DbO7cIxn71CgxONqrt/f+6l8MU9UOpxZDQmoyTneshd0tscJc4 14 | 1gmcuv0GNMX34eiPEY5UIVKTKB9airVOIiN5eendiLAeJbTYRyc9B+2To93CcxzP 15 | 2c5L7znFdmcXzF2skpQOYZf5T5rm5qk58uWYTO2J4Y5Img6oVaiTOh4YoE1AUiZP 16 | O9a9Um+6cayBrxxf61zpJdTnRKlJWQp7rjU/4nixUE/Bsg9oz7ZZ9oc+63DYUrj9 17 | WHoHn8ggJb+b+AilgWTNF5b1B3f0D0qkhnlVFMfqcDJBFPWpVasZvy4eLQIDAQAB 18 | oyEwHzAdBgNVHQ4EFgQUrduUN9yaddnYtC2CE7KeLwxYmY4wDQYJKoZIhvcNAQEL 19 | BQADggIBACLijpWbg5UbScP/bO35jiUFYShwdTu5YKhnTyfoHc2fpTwvrEXjdcEv 20 | vbCGEQEHQgifsJBbX6P6Sy7lA3sKnxuiZUcoV46XMEL4yhbqDcQGqjKYIgwpL26D 21 | OzuRRh+iTAxtW9IYGdqiBw/yq2ij338aPRQ7Gm/3CHG5VMiFnIygfvshEOiiSOoW 22 | Ujdywv3NJHyY2yE8RaTxYmCMDQmlRgpeDCUqf+hd477vMRRPRNVJEtpZl9/TtQA5 23 | BFDr7uM5JN9Z7ntl9GLZXD1I0SR73uYHJAVG6qHz4F62mx/ATKfqjZFv8em/qFwJ 24 | oInRJCobP1p/fdWz6Ja0k9lWfphSViKa7mqLBokI+HsCjEOFiAgyd2/B9JNxEtyd 25 | jp+ztWaH/XHkUtBIXilnWzQxFyKGLSFMw+A/DcaLQgbXeEj238Bu7GgA4WbXHsFP 26 | C/KMidtQ8QHBemZ+22Zl/2+AlwwqH2fZwZWZOyjoa8Am0NPAYFqXlOORyisIUo7a 27 | rXgU0IydCF3Q5itkHC0gvHDNLdjG84LEHSR+LvaxZuznaUL9HeWUF/ssRf4mANWt 28 | 8Lj08BxW+dkrGX8ohnO0mvMku/i9NYGYp/8m0uknDpA3nzI2UKn1+fvf0LDI/va4 29 | zNJEt4IKkOvC++Y+Mnf01LANqmvIrIqXxWghJdpFZi8nuXXdxhtB 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /examples/play_wav/c3lingo-test-channel.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c3lingo/c3lingo-mumble/20d5b8f1751c0c609d9d47341eb4b3804aef29dd/examples/play_wav/c3lingo-test-channel.wav -------------------------------------------------------------------------------- /examples/play_wav/divoc-test-1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # run the example with: 3 | # python -m c3lingo_mumble.play_wav -c examples/play_wav/test-channel.yaml 4 | file: ./C-RadaR-04-2020.wav 5 | #file: examples/play_wav/c3lingo-test-channel.wav 6 | #channel: Lounge 7 | channel: DiVOC-Feinler-Translation-1 8 | host: mumble.c3lingo.org 9 | user: DiVOC-Test-1 10 | certfile: certs/DiVOC-Test-1-cert.pem 11 | keyfile: certs/DiVOC-Test-1-key.pem 12 | reconnect: true 13 | loop: true 14 | -------------------------------------------------------------------------------- /examples/play_wav/divoc-test-2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # run the example with: 3 | # python -m c3lingo_mumble.play_wav -c examples/play_wav/test-channel.yaml 4 | file: ./C-RadaR-04-2020.wav 5 | #file: examples/play_wav/c3lingo-test-channel.wav 6 | #channel: Lounge 7 | channel: DiVOC-Feinler-Translation-2 8 | host: mumble.c3lingo.org 9 | user: DiVOC-Test-1 10 | certfile: certs/DiVOC-Test-2-cert.pem 11 | keyfile: certs/DiVOC-Test-2-key.pem 12 | reconnect: true 13 | loop: true 14 | -------------------------------------------------------------------------------- /examples/play_wav/saal_1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # run the example with: 3 | # python -m c3lingo_mumble.play_wav -c examples/play_wav/test-channel.yaml 4 | file: examples/play_wav/c3lingo-test-channel.wav 5 | channel: Saal_1-original 6 | host: mumble.c3lingo.org 7 | user: Saal-1-0 8 | certfile: certs/Saal_1-0-cert.pem 9 | keyfile: certs/Saal_1-0-key.pem 10 | reconnect: true 11 | loop: true 12 | debug: 0 13 | level: -50 14 | -------------------------------------------------------------------------------- /examples/play_wav/test-channel.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # run the example with: 3 | # python -m c3lingo_mumble.play_wav -c examples/play_wav/test-channel.yaml 4 | file: examples/play_wav/c3lingo-test-channel.wav 5 | channel: test 6 | host: my-mumble-server.example.com 7 | user: test 8 | certfile: certs/test-cert.pem 9 | keyfile: certs/test-key.pem 10 | reconnect: true 11 | loop: true 12 | -------------------------------------------------------------------------------- /examples/register.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Register one or more certificates with the server 5 | # 6 | 7 | host="$1" 8 | admin="$2" 9 | shift 2 10 | 11 | for i in $@; do 12 | pipenv run python -m c3lingo_mumble.register --host ${host} \ 13 | --admin ${admin} --admin_certfile=certs/${admin}-cert.pem --admin_keyfile=certs/${admin}-key.pem \ 14 | --user=${i} --certfile=certs/${i}-cert.pem --keyfile=certs/${i}-key.pem 15 | done 16 | -------------------------------------------------------------------------------- /examples/send_pyaudio/adams.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | sources: 3 | 'snd_rpi_hifiberry_dacplusadc': 4 | 0: 5 | channel: Adams-1 6 | server: 7 | host: my-mumble-server.example.com 8 | user: adams-1 9 | certfile: certs/adams-1-cert.pem 10 | keyfile: certs/adams-1-key.pem 11 | reconnect: true 12 | 1: 13 | channel: Adams-2 14 | server: 15 | host: my-mumble-server.example.com 16 | user: adams-2 17 | certfile: certs/adams-2-cert.pem 18 | keyfile: certs/adams-2-key.pem 19 | reconnect: true 20 | -------------------------------------------------------------------------------- /examples/send_stdin/adams.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # 3 | # Use this example with a source with three audio channels. 4 | # 5 | # With Voctomix as a source: 6 | # ffmpeg -i 35c3-9723-deu-eng-spa-Smart_Home_-_Smart_Hack_hd.mkv -filter_complex '[0:a]pan=3c|c0=.5*c0+.5*c1|c1=.5*c2+.5*c3|c2=.5*c4+.5*c5[a0]' -map '[a0]' -ac 3 -f s16le -c:a pcm_s16le -y - | pipenv run python -m c3lingo_mumble.send_stdin -f adams.yml 7 | # 8 | channels: 9 | - channel: Adams-0 10 | server: 11 | host: my-mumble-server.example.com 12 | user: adams-1 13 | certfile: certs/adams-0-cert.pem 14 | keyfile: certs/adams-0-key.pem 15 | reconnect: true 16 | - channel: Adams-1 17 | server: 18 | host: my-mumble-server.example.com 19 | user: adams-1 20 | certfile: certs/adams-1-cert.pem 21 | keyfile: certs/adams-1-key.pem 22 | reconnect: true 23 | - channel: Adams-2 24 | server: 25 | host: my-mumble-server.example.com 26 | user: adams-2 27 | certfile: certs/adams-2-cert.pem 28 | keyfile: certs/adams-2-key.pem 29 | reconnect: true 30 | -------------------------------------------------------------------------------- /examples/send_stdin/mumblesender.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Send Audio to Mumble Server 3 | After=mumblesender.service 4 | 5 | [Service] 6 | User=mumblesender 7 | Restart=always 8 | Type=simple 9 | WorkingDirectory=/home/mumblesender/c3lingo 10 | ExecStart=ffmpeg -loglevel error -i tcp://localhost:15000 -filter_complex '[0:a]pan=3c|c0=.5*c0+.5*c1|c1=.5*c2+.5*c3|c2=.5*c4+.5*c5[a0]' -map '[a0]' -ac 3 -f s16le -c:a pcm_s16le -y - | pipenv run python -m c3lingo_mumble.send_stdin -f c3lingo-adams.yaml 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /examples/send_stdin/saal_1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # 3 | # Use this example with a source with three audio channels. 4 | # 5 | # With a recording as the source: 6 | # ffmpeg -i ~/Downloads/35c3-9723-deu-eng-spa-Smart_Home_-_Smart_Hack_hd.mp4 -filter_complex '[0:a:0]pan=1c|c03channel.pcm 7 | # pipenv run python -m c3lingo_mumble.send_stdin -f examples/send_stdin/saal_1.yaml <3channel.pcm 8 | # 9 | channels: 10 | - channel: Saal_1-original 11 | server: 12 | host: mumble.c3lingo.org 13 | user: Saal_1-0 14 | certfile: certs/Saal_1-0-cert.pem 15 | keyfile: certs/Saal_1-0-key.pem 16 | reconnect: true 17 | level: -36 18 | - channel: Saal_1-translation-1 19 | server: 20 | host: mumble.c3lingo.org 21 | user: Saal_1-1 22 | certfile: certs/Saal_1-1-cert.pem 23 | keyfile: certs/Saal_1-1-key.pem 24 | reconnect: true 25 | level: -36 26 | - channel: Saal_1-translation-2 27 | server: 28 | host: mumble.c3lingo.org 29 | user: Saal_1-2 30 | certfile: certs/Saal_1-2-cert.pem 31 | keyfile: certs/Saal_1-2-key.pem 32 | reconnect: true 33 | level: -36 34 | -------------------------------------------------------------------------------- /gen-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gencert() { 4 | CN="$1" 5 | cat >.openssl.cnf <= '3.6.0' 3 | google==3.0.0 4 | opuslib==3.0.1 5 | protobuf==3.20.3 ; python_version >= '3.7' 6 | pyaudio==0.2.14 7 | -e "git+https://github.com/azlux/pymumble.git@8be2d18ac7324669e1fcd9b37083560fadb9e9e7#egg=pymumble ; python_version >= '3.6'" 8 | pyyaml==6.0.1 ; python_version >= '3.6' 9 | soupsieve==2.5 ; python_version >= '3.8' 10 | --------------------------------------------------------------------------------