├── js ├── jest.setup.js ├── index.js ├── .editorconfig ├── .prettierrc.js ├── jest.config.js ├── .vscode │ └── settings.json ├── pull_request_template.md ├── tsconfig.json ├── README.md ├── src │ ├── util.ts │ ├── __tests__ │ │ └── test.ts │ ├── types.ts │ ├── index.ts │ └── source.ts ├── package.json └── .eslintrc.js ├── test.mp3 ├── Makefile ├── python ├── Makefile ├── docs │ ├── Makefile │ ├── index.rst │ ├── make.bat │ └── conf.py ├── test.py └── replit │ ├── types.py │ └── __init__.py ├── .gitignore ├── README.md └── LICENSE /js/jest.setup.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(30000) 2 | -------------------------------------------------------------------------------- /test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replit/audio-libs/master/test.mp3 -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | execSync('yarn run test:unit', {stdio: 'inherit'}); 4 | 5 | '' -------------------------------------------------------------------------------- /js/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,ts}] 8 | indent_style = space 9 | indent_size = 2 10 | 11 | -------------------------------------------------------------------------------- /js/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'all', 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | printWidth: 100, 7 | bracketSpacing: true, 8 | arrowParens: 'always', 9 | }; 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | .PHONY: test-all docs-all 4 | 5 | 6 | test-all: 7 | @cd js && yarn test 8 | @cd python && make test 9 | 10 | 11 | docs-all: 12 | @cd js && yarn docs 13 | @cd python && make docs-html 14 | 15 | 16 | all: test-all docs-all 17 | -------------------------------------------------------------------------------- /python/Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | .PHONY: test, docs 7 | 8 | 9 | test: 10 | @python3 test.py 11 | 12 | docs: 13 | @cd ./docs && make 14 | 15 | docs-%: 16 | 17 | @echo $(shell echo $@ | cut -c6-) 18 | @cd ./docs && make $(shell echo $@ | cut -c6-) 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | js/node_modules/ 2 | 3 | js/docs/ 4 | js/dist/ 5 | 6 | js/yarn-error.log 7 | js/yarn-debug.log* 8 | js/yarn-error.log* 9 | js/.yarn-integrity 10 | 11 | js/.cache/ 12 | 13 | python/replit/__pycache__ 14 | python/docs/_build 15 | python/docs/_static 16 | python/_docs/_templates 17 | -------------------------------------------------------------------------------- /js/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | testMatch: ['**/__tests__/**/*.+(ts|tsx|js)'], 4 | transform: { 5 | '^.+\\.(ts|tsx)$': 'ts-jest', 6 | }, 7 | globals: { 8 | 'ts-jest': { 9 | diagnostics: false 10 | } 11 | }, 12 | setupFilesAfterEnv: ['./jest.setup.js'] 13 | }; -------------------------------------------------------------------------------- /js/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | { 5 | "language": "typescript", 6 | "autoFix": true 7 | } 8 | ], 9 | "typescript.tsdk": "node_modules/typescript/lib", 10 | "restructuredtext.confPath": "${workspaceFolder}/python/replit" 11 | } 12 | -------------------------------------------------------------------------------- /js/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Why 2 | === 3 | 4 | _Describe what prompted you to make this change, link relevant resources: Asana tasks, Canny report, Slack discussions...etc_ 5 | 6 | What changed 7 | ============ 8 | 9 | _Describe what changed to a level of detail that someone with no context with your PR could be able to review it_ 10 | 11 | Test plan 12 | ========= 13 | 14 | _Describe what you did to test this change to a level of detail that allows your reviewer to test it_ 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repl.it Audio libraries 2 | 3 | Try it out: 4 | + JS / TS library [![Run on Repl.it](https://repl.it/badge/github/replit/audio-libs)](https://repl.it/github/replit/audio-js) ([docs](https://audio-js-docs.allawesome497.repl.co/)) 5 | + Python library (included in the replit package) [demo here](https://repl.it/@AllAwesome497/Audio-Demo#main.py) ([docs](https://replit-docs-python.allawesome497.repl.co/)) 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src/", 4 | "outDir": "./dist/", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "target": "es5", 8 | "strict": true, 9 | "lib": ["es6"], 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "sourceMap": true, 13 | "declaration": true, 14 | "noImplicitAny": true, 15 | "removeComments": true, 16 | "downlevelIteration": true, 17 | "typeRoots": [ 18 | "node_modules/@types" 19 | ], 20 | }, 21 | "include": ["src"], 22 | "exclude": ["src/**/__tests__"] 23 | } 24 | -------------------------------------------------------------------------------- /python/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile clean 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile clean 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | # README! 2 | 3 | 4 | This is a boilerplate for typescript packages. Find and replace "`somepackage`" with the name of your package. 5 | 6 | ✔️ Compiles your package into commonjs using the typescript compiler - `yarn build` 7 | 8 | ✔️ Bundles the package for browser usage using parcel - `yarn build:browser` 9 | 10 | ✔️ Test your package using jest (tests go under `src/**/__tests__/**`) - `yarn test:unit` 11 | 12 | ✔️ Has eslint and prettier setup - `yarn test:lint && yarn test:format` 13 | 14 | ✔️ Has a precommit hook for auto-fixing formatting and linting using lint-staged [supports partial commits](https://medium.com/hackernoon/announcing-lint-staged-with-support-for-partially-staged-files-abc24a40d3ff) 15 | 16 | ✔️ Can generate docs from the code using [typedoc](https://typedoc.org/) - `yarn docs` 17 | -------------------------------------------------------------------------------- /python/docs/index.rst: -------------------------------------------------------------------------------- 1 | .. replit documentation master file, created by 2 | sphinx-quickstart on Mon Jun 22 18:35:18 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | (python) replit api reference. 7 | ============================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | Module contents 14 | --------------- 15 | 16 | .. automodule:: replit 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | 21 | replit.types module 22 | ------------------- 23 | 24 | .. automodule:: replit.types 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | 30 | 31 | Indices and tables 32 | ================== 33 | 34 | * :ref:`genindex` 35 | * :ref:`modindex` 36 | * :ref:`search` 37 | -------------------------------------------------------------------------------- /python/docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Repl.it 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /js/src/util.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import { AudioStatus, SourceData, SourceNotFoundError } from './types'; 3 | 4 | const audioStatusPath = '/tmp/audioStatus.json'; 5 | 6 | /** 7 | * Returns the raw status data in /tmp/audioStatus.json 8 | * 9 | * This is an api call, you shouldn't need this for general usage. 10 | */ 11 | export async function getAudioStatus(): Promise { 12 | try { 13 | await fs.access(audioStatusPath); 14 | } catch (e) { 15 | // no status found, exit 16 | return null; 17 | } 18 | 19 | const data = await fs.readFile(audioStatusPath, { encoding: 'utf8' }); 20 | if (!data) { 21 | return null; 22 | } 23 | 24 | let audioStatus: AudioStatus; 25 | try { 26 | audioStatus = JSON.parse(data); 27 | } catch (e) { 28 | return null; 29 | } 30 | 31 | // TODO maybe verify data? 32 | 33 | return audioStatus; 34 | } 35 | /** 36 | * This returns a SourceData object with the given ID. 37 | * 38 | * This is an api call, Source objects are returned when they are created. 39 | */ 40 | export async function getRawSource(id: number): Promise { 41 | const data = await getAudioStatus(); 42 | 43 | if (!data) { 44 | throw new SourceNotFoundError(`Could not find source with ID "${id}.`); 45 | } 46 | 47 | let source: SourceData | null = null; 48 | for (const s of data.Sources) { 49 | if (s.ID === id) { 50 | source = s; 51 | break; 52 | } 53 | } 54 | 55 | if (!source) { 56 | throw new SourceNotFoundError(`Could not find source with ID "${id}.`); 57 | } 58 | 59 | return source; 60 | } 61 | 62 | export async function sleep(timeMs: number) { 63 | return new Promise((r) => setTimeout(r, timeMs)); 64 | } 65 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@replit/audio", 3 | "version": "0.0.1", 4 | "description": "description", 5 | "files": [ 6 | "/dist" 7 | ], 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/replit/audio-js" 11 | }, 12 | "main": "./dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "scripts": { 15 | "build": "rm -rf dist && tsc", 16 | "prepublishOnly": "yarn build", 17 | "docs": "typedoc --out docs --name @replit/audio --includeDeclarations --excludeExternals src/index.ts src/source.ts src/types.ts", 18 | "test": "yarn test:lint && yarn test:format && yarn test:unit", 19 | "test:unit": "jest", 20 | "test:lint": "eslint src/ --ext .js,.ts,.tsx", 21 | "test:format": "prettier --check \"src/**/*.{js,json,ts,tsx}\"" 22 | }, 23 | "author": "faris@repl.it", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@types/jest": "^24.0.24", 27 | "@types/node": "10", 28 | "@typescript-eslint/eslint-plugin": "^2.3.0", 29 | "@typescript-eslint/parser": "^2.3.0", 30 | "eslint": "^6.7.2", 31 | "eslint-config-airbnb": "^18.0.1", 32 | "eslint-config-prettier": "^6.3.0", 33 | "eslint-import-resolver-typescript": "^1.1.1", 34 | "eslint-plugin-import": "^2.18.2", 35 | "eslint-plugin-jest": "^23.1.1", 36 | "eslint-plugin-jsx-a11y": "^6.2.3", 37 | "eslint-plugin-prettier": "^3.1.1", 38 | "eslint-plugin-react": "^7.14.3", 39 | "husky": ">=1", 40 | "jest": "^24.9.0", 41 | "lint-staged": ">=8", 42 | "parcel": "^1.12.4", 43 | "prettier": "^1.18.2", 44 | "ts-jest": "^24.2.0", 45 | "typedoc": "^0.15.0", 46 | "typescript": "^3.7.3" 47 | }, 48 | "husky": { 49 | "hooks": { 50 | "pre-commit": "lint-staged" 51 | } 52 | }, 53 | "lint-staged": { 54 | "*.{js,ts,tsx}": [ 55 | "eslint --fix", 56 | "prettier --write", 57 | "git add" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /python/test.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | import replit 4 | from replit import audio, types 5 | from replit.types import WaveType 6 | 7 | test_file = '../test.mp3' 8 | 9 | 10 | class TestAudio(unittest.TestCase): 11 | 12 | def test_creation(self): 13 | source = audio.play_file(test_file) 14 | self.assertEqual(source.path, test_file) 15 | source.paused = True 16 | time.sleep(1) 17 | self.assertEqual(source.paused, True, 'Pausing Source') 18 | 19 | def test_pause(self): 20 | source = audio.play_file(test_file) 21 | source.volume = 2 22 | time.sleep(1) 23 | self.assertEqual(source.volume, 2, "Volume set to 2") 24 | 25 | source.paused = True 26 | time.sleep(1) 27 | self.assertEqual(source.paused, True, 'Pausing Source') 28 | 29 | source.volume = .2 30 | time.sleep(1) 31 | self.assertEqual(source.volume, .2, 'Volume set to .2') 32 | 33 | source.paused = True 34 | time.sleep(1) 35 | self.assertEqual(source.paused, True, 'Pausing Source') 36 | 37 | def test_loop_setting(self): 38 | source = audio.play_file(test_file) 39 | 40 | self.assertEqual(source.loops_remaining, 0, '0 loops remaining') 41 | source.set_loop(2) 42 | time.sleep(1) 43 | 44 | self.assertEqual(source.loops_remaining, 2, '2 loops remaining') 45 | source.paused = True 46 | time.sleep(1) 47 | self.assertEqual(source.paused, True, 'Pausing Source') 48 | 49 | def test_other(self): 50 | source = audio.play_file(test_file) 51 | 52 | self.assertIsNotNone(source.end_time) 53 | self.assertIsNotNone(source.start_time) 54 | self.assertIsNotNone(source.remaining) 55 | source.paused = True 56 | time.sleep(1) 57 | self.assertEqual(source.paused, True, 'Pausing Source') 58 | 59 | def test_tones(self): 60 | try: 61 | audio.play_tone(2, 400, 2) 62 | except TimeoutError or ValueError as e: 63 | self.fail(e) 64 | 65 | 66 | if __name__ == '__main__': 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /js/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: ['airbnb', 'plugin:@typescript-eslint/recommended', 'prettier/@typescript-eslint'], 4 | plugins: ['@typescript-eslint', 'prettier'], 5 | settings: { 6 | 'import/parsers': { 7 | '@typescript-eslint/parser': ['.ts'], 8 | }, 9 | 'import/resolver': { 10 | typescript: {}, 11 | }, 12 | }, 13 | rules: { 14 | 'import/no-extraneous-dependencies': [ 15 | 'error', 16 | { devDependencies: false, peerDependencies: true }, 17 | ], 18 | 'import/prefer-default-export': 'off', 19 | 'import/extensions': [ 20 | 'error', 21 | { 22 | ts: 'never', 23 | }, 24 | ], 25 | indent: 'off', 26 | 'max-len': ['error', { code: 120 }], 27 | '@typescript-eslint/explicit-function-return-type': 'off', 28 | '@typescript-eslint/no-non-null-assertion': 'error', 29 | '@typescript-eslint/no-explicit-any': 'off', 30 | '@typescript-eslint/no-use-before-define': ['error', { functions: false, classes: false }], 31 | '@typescript-eslint/no-unused-vars': [ 32 | 'error', 33 | { 34 | varsIgnorePattern: '^_', 35 | argsIgnorePattern: '^_', 36 | caughtErrorsIgnorePattern: '^ignore', 37 | }, 38 | ], 39 | 'operator-linebreak': 'off', 40 | 'no-param-reassign': ['error', { props: false }], 41 | 'object-curly-newline': 'off', 42 | 'no-plusplus': 'off', 43 | 'newline-before-return': 'error', 44 | 'no-continue': 'off', 45 | 'no-restricted-syntax': 'off', 46 | }, 47 | globals: { 48 | BigInt: true, 49 | }, 50 | overrides: [ 51 | { 52 | files: ['**/__tests__/**/*.+(ts|tsx|js)'], 53 | env: { 54 | 'jest/globals': true, 55 | }, 56 | plugins: ['jest'], 57 | rules: { 58 | 'import/no-extraneous-dependencies': [ 59 | 'error', 60 | { devDependencies: true, peerDependencies: true }, 61 | ], 62 | 'jest/no-disabled-tests': 'warn', 63 | 'jest/no-focused-tests': 'error', 64 | 'jest/no-identical-title': 'error', 65 | 'jest/prefer-to-have-length': 'warn', 66 | 'jest/valid-expect': 'error', 67 | }, 68 | }, 69 | ], 70 | }; 71 | -------------------------------------------------------------------------------- /python/docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../')) 16 | sys.path.append(os.path.abspath('../')) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'replit' 22 | copyright = '2020, repl.it' 23 | author = 'repl.it' 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = '1.2.0' 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.napoleon', 37 | 'sphinx_autodoc_typehints' 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # List of patterns, relative to source directory, that match files and 44 | # directories to ignore when looking for source files. 45 | # This pattern also affects html_static_path and html_extra_path. 46 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'conf.py'] 47 | 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | 51 | # The theme to use for HTML and HTML Help pages. See the documentation for 52 | # a list of builtin themes. 53 | # 54 | html_theme = 'groundwork' 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | html_static_path = ['_static'] 60 | -------------------------------------------------------------------------------- /js/src/__tests__/test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { playFile, playTone } from '../index'; 3 | import { getRawSource, sleep } from '../util'; 4 | import { WaveType } from '../types'; 5 | 6 | describe('Creates sources', () => { 7 | test('Succesfully creates a source', async () => { 8 | const filePath = path.join(__dirname, '../test.mp3'); 9 | 10 | const source = await playFile({ filePath }); 11 | expect(source.filePath).toEqual(filePath); 12 | }); 13 | 14 | test('Can pause source', async () => { 15 | const filePath = path.join(__dirname, '../test.mp3'); 16 | 17 | const source = await playFile({ filePath }); 18 | expect(source.filePath).toEqual(filePath); 19 | 20 | source.togglePlaying(); 21 | await sleep(1000); 22 | expect((await getRawSource(source.ID)).Paused).toEqual(true); 23 | 24 | source.togglePlaying(); 25 | await sleep(1000); 26 | expect((await getRawSource(source.ID)).Paused).toEqual(false); 27 | }); 28 | 29 | test('Can change volume', async () => { 30 | const filePath = path.join(__dirname, '../test.mp3'); 31 | 32 | const source = await playFile({ filePath }); 33 | 34 | source.setVolume(2); 35 | await sleep(1000); 36 | expect((await getRawSource(source.ID)).Volume).toEqual(2); 37 | 38 | source.setVolume(0.2); 39 | await sleep(1000); 40 | expect((await getRawSource(source.ID)).Volume).toEqual(0.2); 41 | }); 42 | 43 | test('Can set loop', async () => { 44 | const filePath = path.join(__dirname, '../test.mp3'); 45 | 46 | const source = await playFile({ filePath }); 47 | 48 | expect((await getRawSource(source.ID)).Loop).toEqual(0); 49 | expect(await source.getRemainingLoops()).toEqual(0); 50 | 51 | source.setLoop(2); 52 | await sleep(1000); 53 | expect((await getRawSource(source.ID)).Loop).toEqual(2); 54 | expect(await source.getRemainingLoops()).toEqual(2); 55 | }); 56 | 57 | test('Other functions return properly', async () => { 58 | const filePath = path.join(__dirname, '../test.mp3'); 59 | 60 | const source = await playFile({ filePath }); 61 | expect(await source.getStartTime).toBeTruthy(); 62 | expect(await source.getEndTime).toBeTruthy(); 63 | expect(await source.getTimeRemaining).toBeTruthy(); 64 | }); 65 | 66 | test('Can play a tone', async () => { 67 | await playTone({ 68 | seconds: 2, 69 | pitch: 400, 70 | type: WaveType.WaveSine, 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /js/src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The raw data for a source. 3 | */ 4 | export interface SourceData { 5 | Name: string; 6 | FileType: string; 7 | Volume: number; 8 | Duration: number; 9 | Remaining: number; 10 | Paused: boolean; 11 | Loop: number; 12 | ID: number; 13 | EndTime: string; 14 | StartTime: string; 15 | Request: RequestData; 16 | } 17 | 18 | /** 19 | * The enum for the available tone wave types. 20 | */ 21 | export enum WaveType { 22 | WaveSine = 0, 23 | WaveTriangle, 24 | WaveSaw, 25 | WaveSqr, 26 | } 27 | 28 | /** 29 | * The enum type for the different source types. 30 | */ 31 | export enum ReaderType { 32 | WavFile = 'wav', 33 | AiffFile = 'aiff', 34 | MP3File = 'mp3', 35 | Tone = 'tone', 36 | } 37 | 38 | /** 39 | * The file types in an array. 40 | */ 41 | export const FileTypes: Array = [ 42 | ReaderType.WavFile, 43 | ReaderType.AiffFile, 44 | ReaderType.MP3File, 45 | ]; 46 | 47 | /** 48 | * Arguments in a request. 49 | */ 50 | export interface RequestArgs { 51 | /** 52 | * The pitch/frequency of the tone. 53 | */ 54 | Pitch?: number; 55 | /** 56 | * The duration for the tone to be played. 57 | */ 58 | Seconds?: number; 59 | /** 60 | * The wave type of the tone. 61 | */ 62 | WaveType?: WaveType; 63 | // {"Volume":1,"DoesLoop":false,"LoopCount":0,"Type":"mp3","Name":"2","Args":{"Path":"../te 64 | // st.mp3"}} 65 | 66 | /** 67 | * Path to the file (if reading from a file) 68 | */ 69 | Path?: string; 70 | } 71 | /** 72 | * The data / payload for a request. 73 | */ 74 | export interface RequestData { 75 | /** 76 | * The ID of the source, only needed if used for updating a prexisting source. 77 | */ 78 | ID?: number; 79 | /** 80 | * Wether the source should be paused or not, can only be set when updating a source. 81 | */ 82 | Paused?: boolean; 83 | /** 84 | * The volume the source should be set to. 85 | */ 86 | Volume: number; 87 | /** 88 | * Wether the source loops or not. 89 | */ 90 | DoesLoop: boolean; 91 | /** 92 | * How many times the source should loop. 93 | */ 94 | LoopCount: number; 95 | /** 96 | * The arguments needed for the source's type. 97 | */ 98 | Args: RequestArgs; 99 | 100 | /** 101 | * The name of the reader. If not provided, pid1 will set it to the decoders id. 102 | */ 103 | Name?: string; 104 | 105 | /** 106 | * The type of the source. 107 | */ 108 | Type: ReaderType; 109 | } 110 | 111 | /** 112 | * The raw data read from /tmp/audioStatus.json 113 | */ 114 | export interface AudioStatus { 115 | Sources: Array; 116 | Running: boolean; 117 | Disabled: boolean; 118 | } 119 | 120 | /** 121 | * The error thrown if a source isn't found. 122 | */ 123 | export class SourceNotFoundError extends Error { 124 | constructor(message: string) { 125 | super(message); 126 | this.name = 'SourceNotFoundError'; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /python/replit/types.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from typing_extensions import TypedDict 3 | from enum import Enum 4 | 5 | 6 | class ReaderType(Enum): 7 | 'An Enum for the types of sources.' 8 | 9 | def __str__(self) -> str: 10 | return self._value_ 11 | 12 | def __repr__(self) -> str: 13 | return f'ReaderType.{self._name_}' 14 | 15 | wav_file = 'wav' 16 | 'ReaderType : The type for a .wav file.' 17 | aiff_file = 'aiff' 18 | 'ReaderType : The type for a .aiff file.' 19 | mp3_file = 'mp3' 20 | 'ReaderType : The type for a .mp3 file.' 21 | tone = 'tone' 22 | 'ReaderType : The type for a generated tone.' 23 | 24 | 25 | class WaveType(Enum): 26 | 'The different wave shapes that can be used for tone generation.' 27 | 28 | def __str__(self) -> str: 29 | return self._value_ 30 | 31 | WaveSine = 0 32 | 'WaveType : The WaveSine wave shape.' 33 | WaveTriangle = 1 34 | 'WaveType : The Triangle wave shape.' 35 | WaveSaw = 2 36 | 'WaveType : The Saw wave shape.' 37 | WaveSqr = 3 38 | 'WaveType : The Square wave shape.' 39 | 40 | 41 | file_types: List[ReaderType] = [ReaderType.aiff_file, 42 | ReaderType.wav_file, ReaderType.mp3_file] 43 | 'The different file types for sources in a list.' 44 | 45 | 46 | class RequestArgs(TypedDict, total=False): 47 | 'The additional arguments for a request that are type-specific.' 48 | Pitch: float 49 | 'float : The pitch/frequency of the tone. Only used if the request type is tone.' 50 | Seconds: float 51 | 52 | 'float : The duration for the tone to be played. Only used if the request type is tone.' 53 | WaveType: WaveType or int 54 | 'WaveType : The wave type of the tone. Only used if the request type is tone.' 55 | Path: str 56 | 'str : The path to the file to be read. Only used if the request is for a file type.' 57 | 58 | 59 | class RequestData(TypedDict): 60 | 'A request to pid1 for a source to be played.' 61 | ID: int 62 | 'int : The ID of the source. Only used for updating a pre-existing source.' 63 | Paused: bool or None 64 | 'bool or None : Wether the source with the provided ID should be paused or not. Can only be used when updating a source.' 65 | Volume: float 66 | 'float : The volume the source should be played at. (1 being 100%)' 67 | DoesLoop: bool 68 | 'bool : Wether the source should loop / repeat or not. Defaults to false.' 69 | LoopCount: int 70 | 'int : How many times the source should loop / repeat. Defaults to 0.' 71 | Name: str 72 | 'str : The name of the source.' 73 | Type: ReaderType or str 74 | 'ReaderType : The type of the source.' 75 | Args: RequestArgs 76 | 'RequestArgs : The additional arguments for the source.' 77 | 78 | 79 | class SourceData(TypedDict): 80 | '''A source's raw data, as a payload.''' 81 | Name: str 82 | 'str : The name of the source.' 83 | Type: str 84 | 'str : The type of the source.' 85 | Volume: float 86 | 'float : The volume of the source.' 87 | Duration: int 88 | 'int : The duration of the source in milliseconds.' 89 | Remaining: int 90 | 'int : How many more milliseconds the source will be playing.' 91 | Paused: bool 92 | 'bool : Wether the source is paused or not.' 93 | Loop: int 94 | 'int : How many times the source will loop. If 0, the source will not repeat itself.' 95 | ID: int 96 | 'int : The ID of the source.' 97 | EndTime: str 98 | 'str : The estimated timestamp for when the source will finish playing.' 99 | StartTime: str 100 | 'str : When the source started playing.' 101 | Request: RequestData 102 | 'RequestData : The request used to create the source.' 103 | 104 | 105 | class AudioStatus(TypedDict): 106 | 'The raw data read from /tmp/audioStatus.json.' 107 | Sources: List[SourceData] or None 108 | 'List[SourceData] : The sources that are know to the audio manager.' 109 | Running: bool 110 | 'bool : Wether the audio manager knows any sources or not.' 111 | Disabled: bool 112 | 'bool : Wether the audio manager is disabled or not.' 113 | -------------------------------------------------------------------------------- /js/src/index.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import Source from './source'; 3 | import { getAudioStatus, sleep } from './util'; 4 | import { SourceData, RequestData, FileTypes, ReaderType, WaveType } from './types'; 5 | /** 6 | * A list of known source IDs used 7 | * to determine new sources. 8 | */ 9 | const knownIds: Array = []; 10 | 11 | let namesUsed = 0; 12 | 13 | /** 14 | * Returns a unique name for a decoder. 15 | */ 16 | function getName(): string { 17 | namesUsed++; 18 | 19 | return namesUsed.toString(); 20 | } 21 | 22 | /** 23 | * Waits for pid1 to add the source with the provided name. 24 | */ 25 | async function getNewSource(name: string): Promise { 26 | // Wait forapid1 to pickup new source from /tmp/audio 27 | // we will timeout after 2 seconds if there's no response 28 | let retries = 0; 29 | const getSourceData = async (): Promise => { 30 | retries++; 31 | if (retries > 20) { 32 | throw new Error('Failed to retrieve audio status'); 33 | } 34 | 35 | const audioStatus = await getAudioStatus(); 36 | if (!audioStatus) { 37 | // We don't have an audio status, wait and retry 38 | await sleep(100); 39 | 40 | return getSourceData(); 41 | } 42 | 43 | for (const sourceData of audioStatus.Sources) { 44 | if (sourceData.Name !== name) { 45 | // Check if we there's any source with our filename 46 | continue; 47 | } 48 | 49 | if (knownIds.includes(sourceData.ID)) { 50 | // If it is a known it means we didn't create this 51 | continue; 52 | } 53 | 54 | return sourceData; 55 | } 56 | 57 | // We didn't find our source, wait and retry 58 | await sleep(100); 59 | 60 | return getSourceData(); 61 | }; 62 | 63 | return getSourceData(); 64 | } 65 | 66 | /** 67 | * Used to start playing a file. 68 | */ 69 | export async function playFile({ 70 | /** 71 | * The path to the file. Can be relative. 72 | */ 73 | filePath, 74 | /** 75 | * The volume the file will be played at (1 being 100%). 76 | */ 77 | volume = 1, 78 | /** 79 | * How many times the file should be restarted. 80 | */ 81 | loop = 0, 82 | /** 83 | * The name you want the file to have. 84 | */ 85 | name = getName(), 86 | }: { 87 | filePath: string; 88 | volume?: number; 89 | loop?: number; 90 | name?: string; 91 | }): Promise { 92 | if (typeof filePath !== 'string') { 93 | throw Error('File cannot be null.'); 94 | } 95 | 96 | let isValid = false; 97 | let fileType: ReaderType = ReaderType.WavFile; 98 | for (const type of FileTypes) { 99 | if (filePath.endsWith(`.${type.toString()}`)) { 100 | isValid = true; 101 | fileType = type; 102 | break; 103 | } 104 | } 105 | 106 | if (!isValid) { 107 | throw new Error('Invalid file type.'); 108 | } 109 | 110 | // Check if file exists 111 | try { 112 | await fs.access(filePath); 113 | } catch (e) { 114 | throw new Error('File not found'); 115 | } 116 | 117 | const data: RequestData = { 118 | Volume: volume, 119 | DoesLoop: loop !== 0, 120 | LoopCount: loop, 121 | Type: fileType, 122 | Name: name, 123 | Args: { 124 | Path: filePath, 125 | }, 126 | }; 127 | 128 | const jsonData = JSON.stringify(data); 129 | await fs.writeFile('/tmp/audio', jsonData); 130 | 131 | const payload = await getNewSource(name); 132 | knownIds.push(payload.ID); 133 | 134 | return new Source(payload); 135 | } 136 | 137 | /** 138 | * Used to start playing a tone. 139 | */ 140 | export async function playTone({ 141 | /** 142 | * How long the tone should play. 143 | */ 144 | seconds, 145 | /** 146 | * The type of the tone. 147 | */ 148 | type, 149 | /** 150 | * The frequency of the tone. 151 | */ 152 | pitch, 153 | /** 154 | * The name of the tone. 155 | */ 156 | name = getName(), 157 | /** 158 | * How many times the tone should be played. 159 | */ 160 | loop = 0, 161 | /** 162 | * The volume of the tone (1 being 100%). 163 | */ 164 | volume = 1, 165 | }: { 166 | seconds: number; 167 | type: WaveType; 168 | pitch: number; 169 | name?: string; 170 | loop?: number; 171 | volume?: number; 172 | }): Promise { 173 | const data: RequestData = { 174 | Volume: volume, 175 | Name: name, 176 | Type: ReaderType.Tone, 177 | DoesLoop: loop !== 0, 178 | LoopCount: loop, 179 | Args: { 180 | Seconds: seconds, 181 | WaveType: type, 182 | Pitch: pitch, 183 | }, 184 | }; 185 | 186 | const jsonData = JSON.stringify(data); 187 | 188 | await fs.writeFile('/tmp/audio', jsonData); 189 | 190 | const payload = await getNewSource(name); 191 | 192 | return new Source(payload); 193 | } 194 | -------------------------------------------------------------------------------- /js/src/source.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import { SourceData, RequestData, FileTypes } from './types'; 3 | import { getRawSource } from './util'; 4 | 5 | interface SourceInterface { 6 | /** 7 | * The ID of the source. 8 | */ 9 | ID: number; 10 | 11 | /** 12 | * The path of the file. 13 | * This should be what you 14 | * provided when you created 15 | * the source. 16 | */ 17 | filePath: string | undefined; 18 | 19 | /** 20 | * The volume the source is currently set to. 21 | */ 22 | volume: number; 23 | 24 | /** 25 | * The estimated duration of the file. 26 | */ 27 | duration: number; 28 | 29 | /** 30 | * Wether the source is paused or not. 31 | */ 32 | isPaused: boolean; 33 | 34 | /** 35 | * Toggles between playing and pausing 36 | * returns a boolean indicating play 37 | * status. true means playing 38 | */ 39 | togglePlaying(): Promise; 40 | 41 | /** 42 | * Sets the number of times 43 | * the audio file will loops. 44 | * Negative n will loop forever 45 | * Zero will play once. 46 | */ 47 | setLoop(n: number): Promise; 48 | 49 | /** 50 | * 0 for 0% and 1 for 100% 51 | * You can amplify the volume 52 | */ 53 | setVolume(vol: number): Promise; 54 | 55 | /** 56 | * Get the estimated time (in millaseconds) 57 | * Remaining for the source. 58 | */ 59 | getTimeRemaining(): Promise; 60 | 61 | /** 62 | * Get the estimated end time 63 | * for the source. 64 | */ 65 | getEndTime(): Promise; 66 | 67 | /** 68 | * Get when the source started 69 | * playing on the current loop. 70 | */ 71 | getStartTime(): Promise; 72 | 73 | /** 74 | * Get the remaining times the 75 | * source will restart. 76 | */ 77 | getRemainingLoops(): Promise; 78 | } 79 | 80 | export default class Source implements SourceInterface { 81 | /** 82 | * The ID of the source. 83 | */ 84 | ID: number; 85 | 86 | /** 87 | * The path of the file. 88 | * This should be what you 89 | * provided when you created 90 | * the source. 91 | */ 92 | filePath: string | undefined; 93 | 94 | /** 95 | * The volume the source is currently set to. 96 | */ 97 | volume: number; 98 | 99 | private loop: number; 100 | 101 | /** 102 | * The estimated duration of the file. 103 | * (In milliseconds) 104 | */ 105 | duration: number; 106 | 107 | /** 108 | * Wether the source is paused or not. 109 | */ 110 | isPaused: boolean; 111 | 112 | /** 113 | * The name of the source. 114 | */ 115 | name: string; 116 | 117 | /** 118 | * The request used to get this source. 119 | */ 120 | request: RequestData; 121 | 122 | constructor(payload: SourceData) { 123 | this.volume = payload.Volume; 124 | this.loop = payload.Loop; 125 | this.duration = payload.Duration; 126 | this.ID = payload.ID; 127 | if (FileTypes.includes(payload.Request.Type)) { 128 | this.filePath = payload.Request.Args.Path; 129 | } 130 | this.isPaused = payload.Paused; 131 | this.name = payload.Name; 132 | this.request = payload.Request; 133 | } 134 | 135 | /** 136 | * Write data to /tmp/audio. 137 | */ 138 | private writeData = async () => { 139 | const data = JSON.stringify({ 140 | ID: this.ID, 141 | Volume: this.volume, 142 | DoesLoop: this.loop !== 0, 143 | LoopCount: this.loop, 144 | Paused: this.isPaused, 145 | }); 146 | await fs.writeFile('/tmp/audio', data); 147 | }; 148 | 149 | /** 150 | * Toggles between playing and pausing 151 | * returns a boolean indicating play 152 | * status. true means playing 153 | */ 154 | togglePlaying = async () => { 155 | this.isPaused = !this.isPaused; 156 | await this.writeData(); 157 | 158 | return this.isPaused; 159 | }; 160 | 161 | /** 162 | * Sets the number of times 163 | * the audio file will loops. 164 | * Negative n will loop forever 165 | * Zero will play once. 166 | */ 167 | setLoop = async (n: number) => { 168 | this.loop = n; 169 | await this.writeData(); 170 | }; 171 | 172 | /** 173 | * Get the estimated time (in millaseconds) 174 | * Remaining for the source. 175 | */ 176 | setVolume = async (n: number) => { 177 | this.volume = n; 178 | await this.writeData(); 179 | }; 180 | 181 | /** 182 | * Get the estimated time (in millaseconds) 183 | * Remaining for the source. 184 | */ 185 | getTimeRemaining = async () => { 186 | const endTime = await this.getEndTime(); 187 | const now = new Date(); 188 | 189 | return endTime.getTime() - now.getTime(); 190 | }; 191 | 192 | /** 193 | * Get the estimated end time 194 | * for the source. 195 | */ 196 | getEndTime = async () => { 197 | const payload = await getRawSource(this.ID); 198 | 199 | return new Date(payload.EndTime); 200 | }; 201 | 202 | /** 203 | * Get when the source started 204 | * playing on the current loop. 205 | */ 206 | getStartTime = async () => { 207 | const payload = await getRawSource(this.ID); 208 | 209 | return new Date(payload.StartTime); 210 | }; 211 | 212 | /** 213 | * Get the remaining times the 214 | * source will restart. 215 | */ 216 | getRemainingLoops = async () => { 217 | const payload = await getRawSource(this.ID); 218 | this.loop = payload.Loop; 219 | 220 | return payload.Loop; 221 | }; 222 | } 223 | -------------------------------------------------------------------------------- /python/replit/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | from replit.types import ReaderType, RequestArgs, RequestData, SourceData, AudioStatus, WaveType, file_types 4 | from typing import List 5 | from datetime import datetime, timedelta 6 | from os import path 7 | 8 | 9 | def clear(): 10 | 'Clear is used to clear the terminal.' 11 | print('\033[H\033[2J', end='', flush=True) 12 | 13 | 14 | class InvalidFileType(Exception): 15 | "Exception for when a requested file's type isnt valid" 16 | pass 17 | 18 | 19 | class NoSuchSourceException(Exception): 20 | "Exception used when a source doesn't exist" 21 | pass 22 | 23 | 24 | class Source: 25 | '''A Source is used to get audio that is sent to the user. 26 | 27 | Parameters 28 | ---------- 29 | payload : :py:class:`~replit.types.SourceData` 30 | The payload for the source. 31 | loops : int 32 | How many times the source should loop. 33 | 34 | ''' 35 | __payload: SourceData 36 | _loops: bool 37 | _name: str 38 | 39 | def __init__(self, payload: SourceData, loops: bool): 40 | self.__payload = payload 41 | self._loops = loops 42 | self._name = payload['Name'] 43 | 44 | def __get_source(self) -> SourceData or None: 45 | source = None 46 | with open('/tmp/audioStatus.json', 'r') as f: 47 | data = json.loads(f.read()) 48 | for s in data['Sources']: 49 | if s['ID'] == self.id: 50 | source = s 51 | break 52 | if source: 53 | self.__payload = source 54 | return source 55 | 56 | def __update_source(self, **changes): 57 | s = self.__get_source() 58 | if not s: 59 | raise NoSuchSourceException( 60 | f'No player with id "{id}" found! It might be done playing.') 61 | s.update({key.title(): changes[key] for key in changes}) 62 | with open('/tmp/audio', 'w') as f: 63 | f.write(json.dumps(s)) 64 | self.__get_source() 65 | 66 | @property 67 | def name(self) -> str: 68 | 'The name of the source' 69 | return self._name 70 | 71 | def get_start_time(self) -> datetime: 72 | 'When the source started plaing' 73 | timestamp_str = self.__payload['StartTime'] 74 | timestamp = datetime.strptime( 75 | timestamp_str[:-4], "%Y-%m-%dT%H:%M:%S.%f") 76 | return timestamp 77 | 78 | start_time: datetime = property(get_start_time) 79 | 'Property wrapper for :py:meth:`~replit.Source.get_start_time`' 80 | 81 | @property 82 | def path(self) -> str or None: 83 | 'The path to the source, if available.' 84 | data = self.__payload 85 | if ReaderType(data['Type']) in file_types: 86 | return self.__payload['Request']['Args']['Path'] 87 | 88 | @property 89 | def id(self) -> int: 90 | 'The ID of the source.' 91 | return self.__payload['ID'] 92 | 93 | def get_remaining(self) -> timedelta: 94 | "The estimated time remaining in the source's current loop." 95 | data = self.__get_source() 96 | if not data: 97 | return timedelta(millaseconds=0) 98 | 99 | return timedelta(milliseconds=data['Remaining']) 100 | 101 | remaining: int = property(get_remaining) 102 | 'Property wrapper for :py:meth:`~replit.Source.get_remaining`' 103 | 104 | def get_end_time(self) -> datetime or None: 105 | '''The estimated time when the sourcce will be done playing. 106 | Returns None if the source has finished playing. 107 | Note: this is the estimation for the end of the current loop.''' 108 | s = self.__get_source() 109 | if not s: 110 | return None 111 | 112 | timestamp_str = s['EndTime'] 113 | timestamp = datetime.strptime( 114 | timestamp_str[:-4], "%Y-%m-%dT%H:%M:%S.%f") 115 | return timestamp 116 | 117 | end_time: datetime or None = property(get_end_time) 118 | 'Property wrapper for :py:meth:`~replit.Source.get_end_time`' 119 | 120 | @property 121 | def does_loop(self) -> bool: 122 | 'Wether the source repeats itself or not.' 123 | return self._loops 124 | 125 | @property 126 | def duration(self) -> timedelta: 127 | 'The duration of the source.' 128 | return timedelta(millaseconds=self.__payload['Duration']) 129 | 130 | def get_volume(self) -> float: 131 | 'The volume the source is set to.' 132 | self.__get_source() 133 | return self.__payload['Volume'] 134 | 135 | def set_volume(self, volume: float): 136 | ''' 137 | Parameters 138 | ---------- 139 | volume: float 140 | The volume the source should be set to. 141 | 142 | Raises 143 | ------ 144 | NoSuchSourceException 145 | If the source is no longer known to the audio manager. 146 | ''' 147 | self.__update_source(volume=volume) 148 | 149 | volume: float = property(get_volume, set_volume) 150 | 'Property wrapper for :py:meth:`~replit.Source.get_volume` and :py:meth:`~replit.Source.set_volume`' 151 | 152 | def get_paused(self) -> bool: 153 | 'Wether the source is paused or not.' 154 | self.__get_source() 155 | return self.__payload['Paused'] 156 | 157 | def set_paused(self, paused: bool): 158 | ''' 159 | Parameters 160 | ---------- 161 | paused: bool 162 | Wether the source should be paused or not. 163 | 164 | Raises 165 | ------ 166 | NoSuchSourceException 167 | If the source is no longer known to the audio manager. 168 | ''' 169 | self.__update_source(paused=paused) 170 | 171 | paused = property(get_paused, set_paused) 172 | 'Property wrapper for :py:meth:`~replit.Source.get_paused` and :py:meth:`~replit.Source.set_paused`' 173 | 174 | def get_loops_remaining(self) -> int or None: 175 | '''The remaining amount of times the file will restart. Returns none if the source is done playing. 176 | 177 | Returns 178 | ------- 179 | int 180 | The number of loops remaining 181 | None 182 | The source can't be found, either because it has finished playing or an error occured. 183 | 184 | ''' 185 | if not self._loops: 186 | return 0 187 | 188 | s = self.__get_source() 189 | if not s: 190 | return None 191 | 192 | if s['ID'] == self.id: 193 | loops = s['Loop'] 194 | 195 | return loops 196 | 197 | def set_loop(self, loop_count: int) -> None: 198 | '''Set the remaining amount of loops for the source. 199 | Set loop_count to a negative value to repeat forever. 200 | 201 | Parameters 202 | ---------- 203 | does_loop: bool 204 | Wether the source should be paused or not. 205 | loop_count: int 206 | How many times the source should repeat itself. Set to a negative value for infinite. 207 | 208 | Raises 209 | ------ 210 | NoSuchSourceException 211 | If the source is no longer known to the audio manager. 212 | ''' 213 | 214 | does_loop = loop_count != 0 215 | self._loops = does_loop 216 | self.__update_source(doesLoop=does_loop, loopCount=loop_count) 217 | 218 | loops_remaining: int or None = property(get_loops_remaining) 219 | 'Property wrapper for :py:meth:`~replit.Source.get_loops_remaining`' 220 | 221 | def toggle_playing(self) -> None: 222 | '''Play/pause the source.''' 223 | self.set_paused(not self.paused) 224 | 225 | 226 | class Audio(): 227 | '''The basic audio manager. 228 | 229 | Notes 230 | ----- 231 | This is not intended to be called directly, instead use :py:const:`audio`. 232 | 233 | Using this in addition to `audio` can cause **major** issues. 234 | ''' 235 | __known_ids = [] 236 | __names_created = 0 237 | 238 | def __gen_name() -> str: 239 | return f'Source {time.time()}' 240 | 241 | def __get_new_source(self, name: str, does_loop: bool) -> Source: 242 | new_source = None 243 | timeOut = datetime.now() + timedelta(seconds=2) 244 | 245 | while not new_source and datetime.now() < timeOut: 246 | try: 247 | sources = AudioStatus(self.read_status())['Sources'] 248 | new_source = SourceData([ 249 | s for s in sources if s['Name'] == name 250 | ][0]) 251 | except IndexError: 252 | pass 253 | except json.JSONDecodeError: 254 | pass 255 | 256 | if not new_source: 257 | raise TimeoutError(f'Source was not created within 2 seconds.') 258 | 259 | return Source(new_source, does_loop) 260 | 261 | def play_file( 262 | self, 263 | file_path: str, 264 | volume: float = 1, 265 | does_loop: bool = False, 266 | loop_count: int = 0, 267 | name: str = __gen_name() 268 | ) -> Source: 269 | '''Sends a request to play a file, assuming the file is valid. 270 | 271 | Parameters 272 | ---------- 273 | file_path: str 274 | The path to the file that should be played. Can be absolute or relative. 275 | volume: float, optional 276 | The volume the source should be played at. (1 being 100%) 277 | does_loop: bool, optional 278 | Wether the source should repeat itself or not. Note, if you set this you should also set loop_count. 279 | loop_count: int, optional 280 | How many times the source should repeat itself. Set to 0 to have the source play only once, 281 | or set to a negative value for the source to repeat forever. 282 | name: str, optional 283 | The name of the file. Default value is a unique name for the source. 284 | 285 | Returns 286 | ------- 287 | Source 288 | The source created with the provided data. 289 | 290 | Raises 291 | ------ 292 | FileNotFoundError 293 | If the file is not found. 294 | InvalidFileType 295 | If the file type is not valid. 296 | ValueError 297 | If the type is not a valid type for a source. 298 | ''' 299 | if not path.exists(file_path): 300 | raise FileNotFoundError(f'File "{file_path}" not found.') 301 | 302 | file_type = file_path.split('.')[-1] 303 | 304 | if ReaderType(file_type) not in file_types: 305 | raise InvalidFileType(f'Type {file_type} is not supported.') 306 | 307 | data = RequestData( 308 | Type=file_type, 309 | Volume=volume, 310 | DoesLoop=does_loop, 311 | LoopCount=loop_count, 312 | Name=name, 313 | Args=RequestArgs( 314 | Path=file_path 315 | ) 316 | ) 317 | 318 | with open('/tmp/audio', 'w') as p: 319 | p.write(json.dumps(dict(data))) 320 | 321 | return self.__get_new_source(name, does_loop) 322 | 323 | def play_tone( 324 | self, 325 | duration: float, 326 | pitch: int, 327 | wave_type: WaveType, 328 | does_loop: bool = False, 329 | loop_count: int = 0, 330 | volume: float = 1, 331 | name: str = __gen_name(), 332 | ) -> Source: 333 | '''Play a tone from a frequency and wave type. 334 | 335 | Parameters 336 | ---------- 337 | duration: float 338 | How long the tone should be played (in seconds). 339 | pitch: int 340 | The frequency the tone should be played at. 341 | wave_type: WaveType 342 | The wave shape used to generate the tone. 343 | volume: float 344 | The volume the tone should be played at (1 being 100%). 345 | name: str 346 | The name of the source. 347 | 348 | Returns 349 | ------- 350 | Source 351 | The source for the tone. 352 | 353 | Raises 354 | ------ 355 | TimeoutError 356 | If the source isn't found after 2 seconds. 357 | ValueError 358 | If the wave type isn't valid. 359 | ''' 360 | 361 | # ensure the wave type is valid. This will throw an error if it isn't. 362 | WaveType(wave_type) 363 | 364 | data = RequestData( 365 | Name=name, 366 | DoesLoop=does_loop, 367 | LoopCount=loop_count, 368 | Volume=volume, 369 | Type=str(ReaderType.tone), 370 | Args=RequestArgs( 371 | WaveType=wave_type, 372 | Pitch=pitch, 373 | Seconds=duration, 374 | ) 375 | ) 376 | 377 | with open('/tmp/audio', 'w') as f: 378 | f.write(json.dumps(data)) 379 | 380 | return self.__get_new_source(name, does_loop) 381 | 382 | def get_source(self, source_id: int) -> Source or None: 383 | '''Get a source by it's ID 384 | 385 | Parameters 386 | ---------- 387 | source_id: int 388 | The ID for the source that should be found. 389 | 390 | Returns 391 | ------- 392 | Source 393 | The source with the ID provided. 394 | 395 | Raises 396 | ------ 397 | :py:exc:`~replit.NoSourceFoundException` 398 | If the source isnt found or there isn't any sources known to the audio manager. 399 | ''' 400 | source = None 401 | with open('/tmp/audioStatus.json', 'r') as f: 402 | data = AudioStatus(json.loads(f.read())) 403 | if not data['Sources']: 404 | raise NoSuchSourceException('No sources exist yet.') 405 | for s in data['Sources']: 406 | 407 | if s['ID'] == int(source_id): 408 | source = s 409 | break 410 | if not source: 411 | raise NoSuchSourceException( 412 | f'Could not find source with ID "{source_id}"') 413 | return Source(source, source['Loop']) 414 | 415 | def read_status(self) -> AudioStatus: 416 | '''Get the raw data for what's playing. This is an api call, and shouldn't be needed 417 | for general usage. 418 | 419 | Returns 420 | ------- 421 | AudioStaus 422 | The contents of /tmp/audioStatus.json 423 | ''' 424 | with open('/tmp/audioStatus.json', 'r') as f: 425 | data = AudioStatus(json.loads(f.read())) 426 | if data['Sources'] == None: 427 | data['Sources']: List[SourceData] = [] 428 | return data 429 | 430 | def get_playing(self) -> List[Source]: 431 | '''Get a list of playing sources. 432 | 433 | Returns 434 | ------- 435 | List[Source] 436 | A list of sources that aren't paused. 437 | ''' 438 | data = self.read_status() 439 | sources = data['Sources'] 440 | return [Source(s, s['Loop']) for s in sources if not s['Paused']] 441 | 442 | def get_paused(self) -> List[Source]: 443 | '''Get a list of paused sources. 444 | 445 | Returns 446 | ------- 447 | List[Source] 448 | A list of sources that are paused. 449 | 450 | ''' 451 | data = self.read_status() 452 | sources = data['Sources'] 453 | return [Source(s, s['Loop']) for s in sources if s['Paused']] 454 | 455 | def get_sources(self) -> List[Source]: 456 | '''Gets all sources. 457 | 458 | Returns 459 | ------- 460 | List[Source] 461 | Every source known to the audio manager, paused or playing. 462 | 463 | ''' 464 | data = self.read_status() 465 | sources = data['Sources'] 466 | return [Source(s, s['Loop']) for s in sources] 467 | 468 | 469 | audio = Audio() 470 | '''The interface used for all things audio. 471 | Can be used to play and fetch audio sources. 472 | 473 | ''' 474 | --------------------------------------------------------------------------------