├── .gitignore ├── README.md ├── favicon.ico ├── index.html ├── main.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── semantic.json ├── server ├── .gitignore ├── db │ ├── connect.js │ └── seed.js ├── middleware │ ├── auth.js │ └── renderer.js ├── models │ ├── Books.js │ ├── Room.js │ ├── Session.js │ └── User.js ├── package-lock.json ├── package.json ├── public │ ├── admin.js │ ├── fav.png │ ├── pitchFinder.js │ ├── script.js │ ├── style.css │ └── user.js ├── routes │ └── api.js └── server.js └── src ├── App.css ├── api └── api.js ├── components ├── AdminView │ └── AdminView.js ├── App │ └── App.js ├── BookCreateForm │ └── BookCreateForm.js ├── BooksList │ └── BooksList.js ├── Confirm │ └── Confim.js ├── LoginForm │ └── LoginForm.js ├── Modal │ └── Modal.js ├── ModalNav │ └── ModalNav.js ├── NavBar │ └── NavBar.js ├── RoomControls │ └── RoomControls.js ├── RoomEdit │ └── RoomEdit.js ├── RoomList │ └── RoomList.js ├── RoomModal │ └── RoomModal.js ├── UserCreateForm │ └── UserCreateForm.js ├── UserView │ └── UserView.js └── UsersList │ └── UsersList.js ├── contexts ├── adminContext.js ├── appContext.js └── userContext.js ├── index.js ├── logo.svg └── serviceWorker.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /server/assets/ 6 | /.pnp 7 | .pnp.js 8 | server.cert 9 | server.key 10 | .env 11 | .mp3 12 | # testing 13 | /coverage 14 | auditorio-win32-x64 15 | release-builds 16 | # production 17 | /build 18 | release-builds/ 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi-Room Control/Monitoring Audio System 2 | #### MERN Stack, WebRTC, Audio API 3 | 4 | #### TL`DR 5 | Imagine a recording studio control room only with multiple recording rooms. 6 | The control room is able to monitor, talkback, control playback, volume and many other element for each room. This is a digital version of an analog system created to help with reading disabilities treatment. 7 | 8 | **[Full article on Medium](https://medium.com/@izhaki.idan/audio-for-learning-disabilities-713ea040e3c6 "Web Audio for learning disabilities 9 | ")** 10 | 11 | #### Can I try it? 12 | 13 | Yep but, Because This is a local project that supposed to work offline I compiled an **Electron Demo Server** that you can download, It also includes some media assets for you to try out. 14 | 15 | ##### Electron Setup 16 | 17 | * **Windows Only** 18 | * Running a mongoDB service is required. 19 | * [Download and unzip the file](https://drive.google.com/open?id=1CGHlPloR0fQEVhB6VTPLaqXyk3sNtgcA "Electron Demo"). 20 | * Run "Audio System.exe" to run the local server. 21 | * Open 2 chrome/firefox tabs for Admin and User 22 | ##### if you are connecting from a remote computer use your IPv4 address instead of localhost. 23 | ##### **Tab 1 - Admin** 24 | * Enter Admin credentials: 25 | * Username: admin 26 | * Password: admin 27 | * Setup Room 0 and save. 28 | ##### **Tab 2 - User** 29 | * Enter User credetials (Change numbers for diffrent rooms): 30 | * Username: Room 0 31 | * Password: Room0 32 | * **Enjoy!** 33 | 34 | #### What can it do? 35 | The application is in its early stages but already implements: 36 | * One central *control room (ADMIN)* that can connect & control and listen to many *listening rooms (USERS)*. 37 | * Admin can recive many connections but send data commands only for one room at a time (While listening...) 38 | * WebRTC (PeerJS) - handles peer 2 peer connection for data and audio streaming connection. 39 | * Web Audio API is used to handle Stream to Audio Context / Audio Context to Stream & Gain control over audio elements. 40 | * Node serverside API handles authentication and data fetching from mongoDB. 41 | * Pitch Detection - *optional* for triggering audio context oscillator over the User playback. 42 | * Heavy workload (Audio Context, Stream Generation & Pitch Detection )implemented on the user side to prevent overloading the admin. 43 | * Node scans assets/books folder and generates media database. 44 | * Audio is buffered and streamed to client. 45 | * Centrelized state managment with React Context System. 46 | * Visual indicators for the online status of each room: 47 | * *grey* - disconnected 48 | * *green* - connected 49 | * *orange* - listening to room 50 | 51 | *While listening to a room admin can:* 52 | 53 | * Start, Stop, Pause, FF, Rewind audio playback 54 | * Hold talkback button to interact with current room 55 | * Activate / Deactive Pitch detection Oscillator and ajust the OSC gain. 56 | * View current room playback time 57 | 58 | ## REQUIREMENTS 59 | * *UPDATED CHROME OR FIREFOX* - for Audio HTML elements stream capture. 60 | * An active MongoDB **local** service. 61 | * *GET OPEN SSL Certificate* - save as server.cert + server.key - for serving HTTPS (Required for proper audio context streams otherwise mose browsers will silence the output). 62 | * *Create media library* - server/assets/books, each subfolder will be logged in the DB, each file in subfolder will be added as parts array. *NOTE:*(This project refers to audio books but you can change the scan path...) 63 | * *SETUP ENV VARIABLES* - PORT, HOST, DEV_DB_URL 64 | * *Run* : npm install, num run build || num run start-build (with nodemon) 65 | * *This project runs localy* to provide service without internet connection. 66 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CannonFodderr/audiosystem/9ec1c03fe8cc2717e1ab22850f9b74c202e185dd/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | Audio System 14 | 15 | 16 |

Audio System Server

17 |

Serving on local IP address, PORT 8080

18 |

Connected to DB

19 | 20 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, Tray, Menu, dialog } = require('electron') 2 | const server = require('./server/server'); 3 | const env = require('dotenv').config(); 4 | const ip = require('ip') 5 | let localIP = ip.address('public', 'ipv4'); 6 | // Get localhost ip 7 | 8 | app.commandLine.appendSwitch('ignore-certificate-errors'); 9 | let appIcon = null; 10 | function createWindow () { 11 | // Create the browser window. 12 | let win = new BrowserWindow({ 13 | width: 400, 14 | height: 150, 15 | autoHideMenuBar: true, 16 | useContentSize: true, 17 | resizable: false, 18 | webPreferences: { 19 | webSecurity: false, 20 | allowDisplayingInsecureContent: true, 21 | allowRunningInsecureContent: true 22 | } 23 | }); 24 | win.setMenu(null); 25 | // win.webContents.openDevTools() 26 | appIcon = new Tray(__dirname + '/favicon.ico'); 27 | let contextMenu = Menu.buildFromTemplate([ 28 | { label: 'Show App', click: function(){ 29 | app.isQuiting = false; 30 | win.show(); 31 | } }, 32 | { label: 'Quit', click: function(){ 33 | app.isQuiting = true; 34 | app.quit(); 35 | } } 36 | ]); 37 | appIcon.setContextMenu(contextMenu); 38 | win.on('minimize',function(event){ 39 | event.preventDefault(); 40 | win.hide(); 41 | return false; 42 | }); 43 | 44 | win.on('close', function (event) { 45 | if(!app.isQuiting){ 46 | event.preventDefault(); 47 | win.hide(); 48 | } 49 | return false; 50 | }); 51 | require('./server/db/connect').then((isConnected) => { 52 | if(!isConnected){ 53 | dialog.showErrorBox('DB ERROR', "Check if MongoDB service is running"); 54 | app.isQuiting = true; 55 | app.quit() 56 | } else { 57 | win.loadFile(__dirname + '/index.html'); 58 | } 59 | win.focus(); 60 | }); 61 | appIcon.setToolTip('Audio System Server'); 62 | appIcon.on('click', () => { 63 | win.show(); 64 | win.focus() 65 | }) 66 | } 67 | 68 | app.on('ready', createWindow); 69 | 70 | server.listen(8080, localIP, () => { 71 | console.log(`Serving on ${localIP}:8080`); 72 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "korenmern", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "main.js", 6 | "productName": "Audio System", 7 | "dependencies": { 8 | "axios": "^0.18.0", 9 | "cors": "^2.8.5", 10 | "dotenv": "^6.2.0", 11 | "formidable": "^1.2.1", 12 | "ip": "^1.1.5", 13 | "peerjs": "^0.3.20", 14 | "pitchfinder": "^2.0.9", 15 | "react": "^16.7.0", 16 | "react-dom": "^16.7.0", 17 | "react-scripts": "^2.1.5", 18 | "react-semantic-ui-range": "^0.6.2", 19 | "semantic-ui-css": "^2.4.1", 20 | "semantic-ui-react": "^0.85.0" 21 | }, 22 | "scripts": { 23 | "start": "electron main.js", 24 | "build": "react-scripts build", 25 | "build-start": "npm run build && electron main.js", 26 | "package-win": "electron-packager . --overwrite --ignore=.mp3 --platform=win32 --arch=x64 --asar --prune=true --out=release-builds --version-string.CompanyName=CE --version-string.FileDescription=CE --version-string.ProductName=\"Audio System\"", 27 | "package-win-with-assets": "electron-packager . --overwrite --platform=win32 --arch=x64 --asar --prune=true --out=release-builds --version-string.CompanyName=CE --version-string.FileDescription=CE --version-string.ProductName=\"Audio System\"" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": [ 33 | ">0.2%", 34 | "not dead", 35 | "not ie <= 11", 36 | "not op_mini all" 37 | ], 38 | "devDependencies": { 39 | "electron": "^4.0.5", 40 | "webpack-cli": "^3.2.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CannonFodderr/audiosystem/9ec1c03fe8cc2717e1ab22850f9b74c202e185dd/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /semantic.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "semantic\\", 3 | "paths": { 4 | "source": { 5 | "config": "src/theme.config", 6 | "definitions": "src/definitions/", 7 | "site": "src/site/", 8 | "themes": "src/themes/" 9 | }, 10 | "output": { 11 | "packaged": "dist\\", 12 | "uncompressed": "dist\\components\\", 13 | "compressed": "dist\\components\\", 14 | "themes": "dist\\themes\\" 15 | }, 16 | "clean": "dist/" 17 | }, 18 | "permission": false, 19 | "autoInstall": false, 20 | "rtl": false, 21 | "components": ["reset", "site", "button", "container", "divider", "flag", "header", "icon", "image", "input", "label", "list", "loader", "rail", "reveal", "segment", "step", "breadcrumb", "form", "grid", "menu", "message", "table", "ad", "card", "comment", "feed", "item", "statistic", "accordion", "calendar", "checkbox", "dimmer", "dropdown", "embed", "modal", "nag", "placeholder", "popup", "progress", "slider", "rating", "search", "shape", "sidebar", "sticky", "tab", "text", "toast", "transition", "api", "form", "state", "visibility"], 22 | "version": "2.7.1" 23 | } 24 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | assets/ 3 | server.cert 4 | server.key 5 | .env 6 | .rnd 7 | .mp3 -------------------------------------------------------------------------------- /server/db/connect.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const env = require('dotenv').config(); 3 | 4 | module.exports = mongoose.connect(process.env.DEV_DB_URL, { useNewUrlParser: true }) 5 | .then(() => { 6 | console.log(`Connected to db`); 7 | return true; 8 | }) 9 | .catch((err) => { 10 | console.log(err); 11 | return false; 12 | }); 13 | mongoose.set('useCreateIndex', true); -------------------------------------------------------------------------------- /server/db/seed.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const User = require('../models/User'); 3 | const Room = require('../models/Room'); 4 | const Book = require('../models/Books'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | const admin = { 9 | firstName: "Admin", 10 | lastName: "istrator", 11 | isAdmin: true, 12 | email: "admin@admin.com" 13 | } 14 | const firstUser = { 15 | firstName: "User", 16 | lastName: "Doe", 17 | email: "user@doe.com" 18 | } 19 | // Create Admin and First User 20 | // *************************** 21 | User.find().then(allUsers => { 22 | if(allUsers.length <= 0){ 23 | User.create(admin).then((createAdmin) =>{ 24 | console.log("Created Admin:", createAdmin); 25 | User.create(firstUser).then((createdUser) => { 26 | console.log("Created Admin:", createdUser); 27 | }); 28 | }) 29 | } 30 | }) 31 | .then(() => { 32 | Room.find().then((allRooms) => { 33 | if(allRooms.length <= 0){ 34 | Room.register(new Room({username: "admin"}), "admin").then((createdLobby) => { 35 | console.log("CreatedLobby:" , createdLobby) 36 | createdLobby.isAdmin = true; 37 | createdLobby.save(); 38 | for(let i = 0; i <= 4; i++){ 39 | let password = `Room${i}` 40 | Room.register(new Room({username: `Room ${i}`}), password) 41 | .then((createdRoom) => { 42 | console.log(createdRoom) 43 | }) 44 | } 45 | }) 46 | } 47 | }) 48 | }) 49 | .then(() => { 50 | Book.find().then((allBooks) => { 51 | if(allBooks.length <= 0){ 52 | const folderPath = path.join(__dirname, '../assets/books'); 53 | const booksFolder = fs.readdirSync(folderPath); 54 | booksFolder.forEach((bookName) => { 55 | const bookPath = `${folderPath}/${bookName}` 56 | const bookFiles = fs.readdirSync(bookPath); 57 | console.log(bookFiles); 58 | Book.create({ 59 | name: bookName, 60 | parts: bookFiles, 61 | }) 62 | .then((createdBook) => console.log("Created Book:", createdBook)) 63 | }); 64 | } 65 | }) 66 | }) 67 | .catch((err) => console.error(err)); 68 | 69 | 70 | // Create Rooms 71 | // ************ 72 | 73 | 74 | 75 | // Create Books 76 | // ************ 77 | 78 | -------------------------------------------------------------------------------- /server/middleware/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | isLoggedIn(req, res, next) { 3 | if (req.user && req.user != 'undefined') { 4 | return next(); 5 | } 6 | return res.redirect('/login'); 7 | }, 8 | isAdmin(req, res, next) { 9 | if(req.user && req.user.isAdmin){ 10 | return next(); 11 | } 12 | return res.redirect('/logout'); 13 | } 14 | } -------------------------------------------------------------------------------- /server/middleware/renderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOMServer from 'react-dom/server' 3 | 4 | // import our main App component 5 | import App from '../src/App'; 6 | 7 | const path = require("path"); 8 | const fs = require("fs"); 9 | 10 | export default (req, res, next) => { 11 | 12 | // point to the html file created by CRA's build tool 13 | const filePath = path.resolve(__dirname, '..', '..', 'build', 'index.html'); 14 | 15 | fs.readFile(filePath, 'utf8', (err, htmlData) => { 16 | if (err) { 17 | console.error('err', err); 18 | return res.status(404).end() 19 | } 20 | 21 | // render the app as a string 22 | const html = ReactDOMServer.renderToString(); 23 | 24 | // inject the rendered app into our html and send it 25 | return res.send( 26 | htmlData.replace( 27 | '
', 28 | `
${html}
` 29 | ) 30 | ); 31 | }); 32 | } -------------------------------------------------------------------------------- /server/models/Books.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const bookSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | unique: true 7 | }, 8 | author: { 9 | type: String 10 | }, 11 | parts: { 12 | type: [String] 13 | }, 14 | path: String 15 | }); 16 | 17 | const Book = mongoose.model('Book', bookSchema); 18 | module.exports = Book; -------------------------------------------------------------------------------- /server/models/Room.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const passportLocalMongoose = require('passport-local-mongoose'); 3 | 4 | const roomSchema = new mongoose.Schema({ 5 | password: { 6 | type: String, 7 | }, 8 | username: { 9 | type: String, 10 | }, 11 | isAdmin: { 12 | type: Boolean, 13 | default: false 14 | }, 15 | currentUser: { 16 | type: mongoose.Schema.Types.ObjectId, 17 | ref: 'User', 18 | default: null 19 | }, 20 | currentBook: { 21 | type: mongoose.Schema.Types.ObjectId, 22 | ref: 'Book', 23 | default: null 24 | }, 25 | currentPart: { 26 | type: String, 27 | default: null 28 | } 29 | }); 30 | 31 | roomSchema.plugin(passportLocalMongoose); 32 | 33 | const Room = mongoose.model('Room', roomSchema); 34 | module.exports = Room; 35 | -------------------------------------------------------------------------------- /server/models/Session.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | 4 | const SessionSchema = new mongoose.Schema({ 5 | user: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'User', 8 | required: true 9 | }, 10 | date: { 11 | type: mongoose.Schema.Types.Date, 12 | default: Date.now() 13 | }, 14 | room: { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: 'Room', 17 | required: true 18 | }, 19 | book: { 20 | type: mongoose.Schema.Types.ObjectId, 21 | ref: 'Book', 22 | required: true 23 | }, 24 | part: { 25 | type: String, 26 | required: true 27 | } 28 | }); 29 | 30 | 31 | 32 | const Session = mongoose.model('Session', SessionSchema); 33 | module.exports = Session; -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | firstName: { 5 | type: String, 6 | }, 7 | lastName: { 8 | type: String 9 | }, 10 | email: { 11 | type: String, 12 | unique: true, 13 | }, 14 | isAdmin: { 15 | type: Boolean, 16 | default: false, 17 | }, 18 | currentRoom: { 19 | type: mongoose.Schema.Types.ObjectId, 20 | ref: 'Room', 21 | default: null 22 | }, 23 | currentBook: { 24 | type: mongoose.Schema.Types.ObjectId, 25 | ref: 'Book', 26 | default: null 27 | }, 28 | lastChapter: { 29 | type: String, 30 | }, 31 | bookList: { 32 | type: [mongoose.Schema.Types.ObjectId], 33 | ref: 'BookList' 34 | }, 35 | sessions: { 36 | type: [mongoose.Schema.Types.ObjectId], 37 | ref: 'Sessions' 38 | } 39 | }); 40 | 41 | const User = mongoose.model('User', UserSchema); 42 | module.exports = User; -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 11 | "requires": { 12 | "mime-types": "~2.1.18", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "array-flatten": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 20 | }, 21 | "async": { 22 | "version": "2.6.1", 23 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", 24 | "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", 25 | "requires": { 26 | "lodash": "^4.17.10" 27 | } 28 | }, 29 | "async-limiter": { 30 | "version": "1.0.0", 31 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 32 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 33 | }, 34 | "bluebird": { 35 | "version": "3.5.1", 36 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", 37 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" 38 | }, 39 | "body-parser": { 40 | "version": "1.18.3", 41 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", 42 | "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", 43 | "requires": { 44 | "bytes": "3.0.0", 45 | "content-type": "~1.0.4", 46 | "debug": "2.6.9", 47 | "depd": "~1.1.2", 48 | "http-errors": "~1.6.3", 49 | "iconv-lite": "0.4.23", 50 | "on-finished": "~2.3.0", 51 | "qs": "6.5.2", 52 | "raw-body": "2.3.3", 53 | "type-is": "~1.6.16" 54 | } 55 | }, 56 | "bson": { 57 | "version": "1.1.0", 58 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz", 59 | "integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA==" 60 | }, 61 | "bytes": { 62 | "version": "3.0.0", 63 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 64 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 65 | }, 66 | "content-disposition": { 67 | "version": "0.5.2", 68 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 69 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 70 | }, 71 | "content-type": { 72 | "version": "1.0.4", 73 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 74 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 75 | }, 76 | "cookie": { 77 | "version": "0.3.1", 78 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 79 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 80 | }, 81 | "cookie-signature": { 82 | "version": "1.0.6", 83 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 84 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 85 | }, 86 | "cors": { 87 | "version": "2.8.5", 88 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 89 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 90 | "requires": { 91 | "object-assign": "^4", 92 | "vary": "^1" 93 | } 94 | }, 95 | "crc": { 96 | "version": "3.4.4", 97 | "resolved": "https://registry.npmjs.org/crc/-/crc-3.4.4.tgz", 98 | "integrity": "sha1-naHpgOO9RPxck79as9ozeNheRms=" 99 | }, 100 | "debug": { 101 | "version": "2.6.9", 102 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 103 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 104 | "requires": { 105 | "ms": "2.0.0" 106 | } 107 | }, 108 | "depd": { 109 | "version": "1.1.2", 110 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 111 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 112 | }, 113 | "destroy": { 114 | "version": "1.0.4", 115 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 116 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 117 | }, 118 | "ee-first": { 119 | "version": "1.1.1", 120 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 121 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 122 | }, 123 | "ejs": { 124 | "version": "2.6.1", 125 | "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", 126 | "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==" 127 | }, 128 | "encodeurl": { 129 | "version": "1.0.2", 130 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 131 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 132 | }, 133 | "escape-html": { 134 | "version": "1.0.3", 135 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 136 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 137 | }, 138 | "etag": { 139 | "version": "1.8.1", 140 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 141 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 142 | }, 143 | "express": { 144 | "version": "4.16.4", 145 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", 146 | "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", 147 | "requires": { 148 | "accepts": "~1.3.5", 149 | "array-flatten": "1.1.1", 150 | "body-parser": "1.18.3", 151 | "content-disposition": "0.5.2", 152 | "content-type": "~1.0.4", 153 | "cookie": "0.3.1", 154 | "cookie-signature": "1.0.6", 155 | "debug": "2.6.9", 156 | "depd": "~1.1.2", 157 | "encodeurl": "~1.0.2", 158 | "escape-html": "~1.0.3", 159 | "etag": "~1.8.1", 160 | "finalhandler": "1.1.1", 161 | "fresh": "0.5.2", 162 | "merge-descriptors": "1.0.1", 163 | "methods": "~1.1.2", 164 | "on-finished": "~2.3.0", 165 | "parseurl": "~1.3.2", 166 | "path-to-regexp": "0.1.7", 167 | "proxy-addr": "~2.0.4", 168 | "qs": "6.5.2", 169 | "range-parser": "~1.2.0", 170 | "safe-buffer": "5.1.2", 171 | "send": "0.16.2", 172 | "serve-static": "1.13.2", 173 | "setprototypeof": "1.1.0", 174 | "statuses": "~1.4.0", 175 | "type-is": "~1.6.16", 176 | "utils-merge": "1.0.1", 177 | "vary": "~1.1.2" 178 | }, 179 | "dependencies": { 180 | "statuses": { 181 | "version": "1.4.0", 182 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 183 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 184 | } 185 | } 186 | }, 187 | "express-session": { 188 | "version": "1.15.6", 189 | "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.6.tgz", 190 | "integrity": "sha512-r0nrHTCYtAMrFwZ0kBzZEXa1vtPVrw0dKvGSrKP4dahwBQ1BJpF2/y1Pp4sCD/0kvxV4zZeclyvfmw0B4RMJQA==", 191 | "requires": { 192 | "cookie": "0.3.1", 193 | "cookie-signature": "1.0.6", 194 | "crc": "3.4.4", 195 | "debug": "2.6.9", 196 | "depd": "~1.1.1", 197 | "on-headers": "~1.0.1", 198 | "parseurl": "~1.3.2", 199 | "uid-safe": "~2.1.5", 200 | "utils-merge": "1.0.1" 201 | } 202 | }, 203 | "finalhandler": { 204 | "version": "1.1.1", 205 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 206 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 207 | "requires": { 208 | "debug": "2.6.9", 209 | "encodeurl": "~1.0.2", 210 | "escape-html": "~1.0.3", 211 | "on-finished": "~2.3.0", 212 | "parseurl": "~1.3.2", 213 | "statuses": "~1.4.0", 214 | "unpipe": "~1.0.0" 215 | }, 216 | "dependencies": { 217 | "statuses": { 218 | "version": "1.4.0", 219 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 220 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 221 | } 222 | } 223 | }, 224 | "forwarded": { 225 | "version": "0.1.2", 226 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 227 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 228 | }, 229 | "fresh": { 230 | "version": "0.5.2", 231 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 232 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 233 | }, 234 | "generaterr": { 235 | "version": "1.5.0", 236 | "resolved": "https://registry.npmjs.org/generaterr/-/generaterr-1.5.0.tgz", 237 | "integrity": "sha1-sM62zFFk3yoGEzjMNAqGFTlcUvw=" 238 | }, 239 | "http-errors": { 240 | "version": "1.6.3", 241 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 242 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 243 | "requires": { 244 | "depd": "~1.1.2", 245 | "inherits": "2.0.3", 246 | "setprototypeof": "1.1.0", 247 | "statuses": ">= 1.4.0 < 2" 248 | } 249 | }, 250 | "iconv-lite": { 251 | "version": "0.4.23", 252 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", 253 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", 254 | "requires": { 255 | "safer-buffer": ">= 2.1.2 < 3" 256 | } 257 | }, 258 | "inherits": { 259 | "version": "2.0.3", 260 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 261 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 262 | }, 263 | "ipaddr.js": { 264 | "version": "1.8.0", 265 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", 266 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" 267 | }, 268 | "kareem": { 269 | "version": "2.3.0", 270 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.0.tgz", 271 | "integrity": "sha512-6hHxsp9e6zQU8nXsP+02HGWXwTkOEw6IROhF2ZA28cYbUk4eJ6QbtZvdqZOdD9YPKghG3apk5eOCvs+tLl3lRg==" 272 | }, 273 | "lodash": { 274 | "version": "4.17.11", 275 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 276 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" 277 | }, 278 | "media-typer": { 279 | "version": "0.3.0", 280 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 281 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 282 | }, 283 | "memory-pager": { 284 | "version": "1.5.0", 285 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 286 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 287 | "optional": true 288 | }, 289 | "merge-descriptors": { 290 | "version": "1.0.1", 291 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 292 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 293 | }, 294 | "methods": { 295 | "version": "1.1.2", 296 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 297 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 298 | }, 299 | "mime": { 300 | "version": "1.4.1", 301 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 302 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 303 | }, 304 | "mime-db": { 305 | "version": "1.37.0", 306 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", 307 | "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" 308 | }, 309 | "mime-types": { 310 | "version": "2.1.21", 311 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", 312 | "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", 313 | "requires": { 314 | "mime-db": "~1.37.0" 315 | } 316 | }, 317 | "minimist": { 318 | "version": "0.0.10", 319 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", 320 | "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" 321 | }, 322 | "mongodb": { 323 | "version": "3.1.10", 324 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.10.tgz", 325 | "integrity": "sha512-Uml42GeFxhTGQVml1XQ4cD0o/rp7J2ROy0fdYUcVitoE7vFqEhKH4TYVqRDpQr/bXtCJVxJdNQC1ntRxNREkPQ==", 326 | "requires": { 327 | "mongodb-core": "3.1.9", 328 | "safe-buffer": "^5.1.2" 329 | } 330 | }, 331 | "mongodb-core": { 332 | "version": "3.1.9", 333 | "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.9.tgz", 334 | "integrity": "sha512-MJpciDABXMchrZphh3vMcqu8hkNf/Mi+Gk6btOimVg1XMxLXh87j6FAvRm+KmwD1A9fpu3qRQYcbQe4egj23og==", 335 | "requires": { 336 | "bson": "^1.1.0", 337 | "require_optional": "^1.0.1", 338 | "safe-buffer": "^5.1.2", 339 | "saslprep": "^1.0.0" 340 | } 341 | }, 342 | "mongoose": { 343 | "version": "5.4.9", 344 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.4.9.tgz", 345 | "integrity": "sha512-4dSQpDUe/9b7A7dRrsyJfWmoGEpeMaZ/WZ/KIJFqTHbJm3NUWaWF++hhirAgjtoHNq2ZILIII0LHEhgzP2NuRw==", 346 | "requires": { 347 | "async": "2.6.1", 348 | "bson": "~1.1.0", 349 | "kareem": "2.3.0", 350 | "mongodb": "3.1.10", 351 | "mongodb-core": "3.1.9", 352 | "mongoose-legacy-pluralize": "1.0.2", 353 | "mpath": "0.5.1", 354 | "mquery": "3.2.0", 355 | "ms": "2.0.0", 356 | "regexp-clone": "0.0.1", 357 | "safe-buffer": "5.1.2", 358 | "sliced": "1.0.1" 359 | } 360 | }, 361 | "mongoose-legacy-pluralize": { 362 | "version": "1.0.2", 363 | "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", 364 | "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" 365 | }, 366 | "mpath": { 367 | "version": "0.5.1", 368 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.5.1.tgz", 369 | "integrity": "sha512-H8OVQ+QEz82sch4wbODFOz+3YQ61FYz/z3eJ5pIdbMEaUzDqA268Wd+Vt4Paw9TJfvDgVKaayC0gBzMIw2jhsg==" 370 | }, 371 | "mquery": { 372 | "version": "3.2.0", 373 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.0.tgz", 374 | "integrity": "sha512-qPJcdK/yqcbQiKoemAt62Y0BAc0fTEKo1IThodBD+O5meQRJT/2HSe5QpBNwaa4CjskoGrYWsEyjkqgiE0qjhg==", 375 | "requires": { 376 | "bluebird": "3.5.1", 377 | "debug": "3.1.0", 378 | "regexp-clone": "0.0.1", 379 | "safe-buffer": "5.1.2", 380 | "sliced": "1.0.1" 381 | }, 382 | "dependencies": { 383 | "debug": { 384 | "version": "3.1.0", 385 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 386 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 387 | "requires": { 388 | "ms": "2.0.0" 389 | } 390 | } 391 | } 392 | }, 393 | "ms": { 394 | "version": "2.0.0", 395 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 396 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 397 | }, 398 | "negotiator": { 399 | "version": "0.6.1", 400 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 401 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 402 | }, 403 | "object-assign": { 404 | "version": "4.1.1", 405 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 406 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 407 | }, 408 | "on-finished": { 409 | "version": "2.3.0", 410 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 411 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 412 | "requires": { 413 | "ee-first": "1.1.1" 414 | } 415 | }, 416 | "on-headers": { 417 | "version": "1.0.1", 418 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", 419 | "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" 420 | }, 421 | "optimist": { 422 | "version": "0.6.1", 423 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", 424 | "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", 425 | "requires": { 426 | "minimist": "~0.0.1", 427 | "wordwrap": "~0.0.2" 428 | } 429 | }, 430 | "parseurl": { 431 | "version": "1.3.2", 432 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 433 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 434 | }, 435 | "passport": { 436 | "version": "0.4.0", 437 | "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", 438 | "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", 439 | "requires": { 440 | "passport-strategy": "1.x.x", 441 | "pause": "0.0.1" 442 | } 443 | }, 444 | "passport-local": { 445 | "version": "1.0.0", 446 | "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", 447 | "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", 448 | "requires": { 449 | "passport-strategy": "1.x.x" 450 | } 451 | }, 452 | "passport-local-mongoose": { 453 | "version": "5.0.1", 454 | "resolved": "https://registry.npmjs.org/passport-local-mongoose/-/passport-local-mongoose-5.0.1.tgz", 455 | "integrity": "sha512-VUY5DgBdpjt1tjunJJ1EXV5b2nhMDkXJuhTjyiK660IgIp7kONMyWEe9tGHf8I9tZudXuTF+47JNQLIzU+Hjbw==", 456 | "requires": { 457 | "debug": "^3.1.0", 458 | "generaterr": "^1.5.0", 459 | "passport-local": "^1.0.0", 460 | "scmp": "^2.0.0", 461 | "semver": "^5.5.0" 462 | }, 463 | "dependencies": { 464 | "debug": { 465 | "version": "3.2.6", 466 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 467 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 468 | "requires": { 469 | "ms": "^2.1.1" 470 | } 471 | }, 472 | "ms": { 473 | "version": "2.1.1", 474 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 475 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 476 | } 477 | } 478 | }, 479 | "passport-strategy": { 480 | "version": "1.0.0", 481 | "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", 482 | "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" 483 | }, 484 | "path-to-regexp": { 485 | "version": "0.1.7", 486 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 487 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 488 | }, 489 | "pause": { 490 | "version": "0.0.1", 491 | "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", 492 | "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" 493 | }, 494 | "peer": { 495 | "version": "0.2.10", 496 | "resolved": "https://registry.npmjs.org/peer/-/peer-0.2.10.tgz", 497 | "integrity": "sha512-G0HACAPv6mK0i+4v/pICouo7YG2qJi5Fkikayfv/+IDovTeOW4VpsRJ9YXe+49MfknXJrc6O93GE7qg7knBEeA==", 498 | "requires": { 499 | "body-parser": "^1.18.3", 500 | "cors": "~2.8.4", 501 | "express": "^4.16.3", 502 | "optimist": "~0.6.1", 503 | "ws": "6.0.0" 504 | } 505 | }, 506 | "proxy-addr": { 507 | "version": "2.0.4", 508 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", 509 | "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", 510 | "requires": { 511 | "forwarded": "~0.1.2", 512 | "ipaddr.js": "1.8.0" 513 | } 514 | }, 515 | "qs": { 516 | "version": "6.5.2", 517 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 518 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 519 | }, 520 | "random-bytes": { 521 | "version": "1.0.0", 522 | "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", 523 | "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" 524 | }, 525 | "range-parser": { 526 | "version": "1.2.0", 527 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 528 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 529 | }, 530 | "raw-body": { 531 | "version": "2.3.3", 532 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", 533 | "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", 534 | "requires": { 535 | "bytes": "3.0.0", 536 | "http-errors": "1.6.3", 537 | "iconv-lite": "0.4.23", 538 | "unpipe": "1.0.0" 539 | } 540 | }, 541 | "regexp-clone": { 542 | "version": "0.0.1", 543 | "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", 544 | "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" 545 | }, 546 | "require_optional": { 547 | "version": "1.0.1", 548 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", 549 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", 550 | "requires": { 551 | "resolve-from": "^2.0.0", 552 | "semver": "^5.1.0" 553 | } 554 | }, 555 | "resolve-from": { 556 | "version": "2.0.0", 557 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", 558 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" 559 | }, 560 | "safe-buffer": { 561 | "version": "5.1.2", 562 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 563 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 564 | }, 565 | "safer-buffer": { 566 | "version": "2.1.2", 567 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 568 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 569 | }, 570 | "saslprep": { 571 | "version": "1.0.2", 572 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz", 573 | "integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==", 574 | "optional": true, 575 | "requires": { 576 | "sparse-bitfield": "^3.0.3" 577 | } 578 | }, 579 | "scmp": { 580 | "version": "2.0.0", 581 | "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.0.0.tgz", 582 | "integrity": "sha1-JHEQ7yLM+JexOj8KvdtSeCOTzWo=" 583 | }, 584 | "semver": { 585 | "version": "5.6.0", 586 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", 587 | "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" 588 | }, 589 | "send": { 590 | "version": "0.16.2", 591 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 592 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 593 | "requires": { 594 | "debug": "2.6.9", 595 | "depd": "~1.1.2", 596 | "destroy": "~1.0.4", 597 | "encodeurl": "~1.0.2", 598 | "escape-html": "~1.0.3", 599 | "etag": "~1.8.1", 600 | "fresh": "0.5.2", 601 | "http-errors": "~1.6.2", 602 | "mime": "1.4.1", 603 | "ms": "2.0.0", 604 | "on-finished": "~2.3.0", 605 | "range-parser": "~1.2.0", 606 | "statuses": "~1.4.0" 607 | }, 608 | "dependencies": { 609 | "statuses": { 610 | "version": "1.4.0", 611 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 612 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 613 | } 614 | } 615 | }, 616 | "serve-static": { 617 | "version": "1.13.2", 618 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 619 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 620 | "requires": { 621 | "encodeurl": "~1.0.2", 622 | "escape-html": "~1.0.3", 623 | "parseurl": "~1.3.2", 624 | "send": "0.16.2" 625 | } 626 | }, 627 | "setprototypeof": { 628 | "version": "1.1.0", 629 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 630 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 631 | }, 632 | "sliced": { 633 | "version": "1.0.1", 634 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", 635 | "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" 636 | }, 637 | "sparse-bitfield": { 638 | "version": "3.0.3", 639 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 640 | "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", 641 | "optional": true, 642 | "requires": { 643 | "memory-pager": "^1.0.2" 644 | } 645 | }, 646 | "statuses": { 647 | "version": "1.5.0", 648 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 649 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 650 | }, 651 | "type-is": { 652 | "version": "1.6.16", 653 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 654 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 655 | "requires": { 656 | "media-typer": "0.3.0", 657 | "mime-types": "~2.1.18" 658 | } 659 | }, 660 | "uid-safe": { 661 | "version": "2.1.5", 662 | "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", 663 | "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", 664 | "requires": { 665 | "random-bytes": "~1.0.0" 666 | } 667 | }, 668 | "unpipe": { 669 | "version": "1.0.0", 670 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 671 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 672 | }, 673 | "utils-merge": { 674 | "version": "1.0.1", 675 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 676 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 677 | }, 678 | "vary": { 679 | "version": "1.1.2", 680 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 681 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 682 | }, 683 | "wordwrap": { 684 | "version": "0.0.3", 685 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", 686 | "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" 687 | }, 688 | "ws": { 689 | "version": "6.0.0", 690 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.0.0.tgz", 691 | "integrity": "sha512-c2UlYcAZp1VS8AORtpq6y4RJIkJ9dQz18W32SpR/qXGfLDZ2jU4y4wKvvZwqbi7U6gxFQTeE+urMbXU/tsDy4w==", 692 | "requires": { 693 | "async-limiter": "~1.0.0" 694 | } 695 | } 696 | } 697 | } 698 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.18.3", 14 | "ejs": "^2.6.1", 15 | "express": "^4.16.4", 16 | "express-session": "^1.15.6", 17 | "mongoose": "^5.4.9", 18 | "passport": "^0.4.0", 19 | "passport-local": "^1.0.0", 20 | "passport-local-mongoose": "^5.0.1", 21 | "peer": "^0.2.10" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/public/admin.js: -------------------------------------------------------------------------------- 1 | // PEER CONNECTION 2 | 3 | // let reconnectAttempts = 0; 4 | let ctx; 5 | let adminMicGain; 6 | let outputToUser; 7 | const startCtx = document.getElementById('startctx'); 8 | const disconnectButton = document.getElementById('disconnect'); 9 | const adminPlayer = document.querySelector('audio'); 10 | const talkBackButton = document.getElementById('talkback'); 11 | const userMicGainSlider = document.querySelector('#micGain'); 12 | const userPlayerGainSlider = document.querySelector('#playerGain'); 13 | const startUserPlayerBtn = document.getElementById('startUserPlayer'); 14 | const pauseUserPlayerBtn = document.getElementById('pauseUserPlayer'); 15 | const stopUserPlayerBtn = document.getElementById('stopUserPlayer'); 16 | const timeDisplay = document.getElementById('timeDisplay'); 17 | const rewind = document.getElementById('rewind'); 18 | const fforward = document.getElementById('fforward'); 19 | const oscActive = document.getElementById('oscActive'); 20 | const oscGain = document.getElementById('oscGain'); 21 | const host = window.location.hostname; 22 | 23 | 24 | // const updateRoomsList = () => { 25 | // const roomsList = document.getElementById('roomList'); 26 | // roomsList.innerHTML = ""; 27 | // for(room in peer.connections){ 28 | // roomsList.innerHTML += `${room}` 29 | // } 30 | // } 31 | 32 | peer.on('disconnected', () => { 33 | location.reload(); 34 | // let reconnectInterval = setInterval(() => { 35 | // if(reconnectAttempts <= 3){ 36 | // peer.reconnect(); 37 | // reconnectAttempts += 1; 38 | // } else { 39 | // console.log("Unable to connect..."); 40 | // clearInterval(reconnectInterval); 41 | // } 42 | // }, 3000); 43 | }) 44 | 45 | peer.on('connection', (conn) => { 46 | conn.on('open', () => { 47 | // startUserPlayerBtn.removeAttribute('disabled'); 48 | conn.send({cmd: "admin connect"}); 49 | console.log('got user online') 50 | // startCtx.removeAttribute('disabled'); 51 | updateRoomsList() 52 | conn.on('data', (data) => { 53 | if(data.cmd === "update"){ 54 | // console.log(data) 55 | // Mic & Player Gain Status 56 | userMicGainSlider.value = data.micGain * 100; 57 | userPlayerGainSlider.value = data.playerGain * 100; 58 | // Playin status 59 | // if(data.isPlaying){ 60 | // startUserPlayerBtn.setAttribute('disabled', true); 61 | // stopUserPlayerBtn.removeAttribute('disabled'); 62 | // pauseUserPlayerBtn.removeAttribute('disabled'); 63 | // } else { 64 | // startUserPlayerBtn.removeAttribute('disabled'); 65 | // stopUserPlayerBtn.setAttribute('disabled', true); 66 | // pauseUserPlayerBtn.setAttribute('disabled', true); 67 | // } 68 | } 69 | if(data.cmd === "player status"){ 70 | let min = Math.floor(data.value / 60) 71 | let sec = Math.round(data.value - min * 60); 72 | let timeString = `${min}:${sec}` 73 | timeDisplay.innerHTML = timeString 74 | } 75 | }) 76 | oscActive.addEventListener('change', (e) => { 77 | if(e.target.value === "on"){ 78 | oscActive.setAttribute('value', "off"); 79 | } else { 80 | oscActive.setAttribute('value', "on"); 81 | } 82 | conn.send({cmd: "osc state", value: e.target.value}); 83 | }) 84 | oscGain.addEventListener('change', (e) => { 85 | conn.send({cmd: "osc gain", value: e.target.value / 10000 }); 86 | }) 87 | userMicGainSlider.addEventListener('change', (e) => { 88 | conn.send({cmd: "mic gain", value: e.target.value / 100 }); 89 | }); 90 | userPlayerGainSlider.addEventListener('change', (e) => { 91 | conn.send({cmd: "player gain", value: e.target.value / 100 }); 92 | }); 93 | talkBackButton.addEventListener('mousedown', () => { 94 | adminMicGain.gain.value = 0.7; 95 | console.log(adminMicGain.gain.value) 96 | conn.send("Talkback open") 97 | }); 98 | talkBackButton.addEventListener('mouseup', () => { 99 | adminMicGain.gain.value = 0; 100 | console.log(adminMicGain.gain.value) 101 | conn.send("Talkback closed") 102 | }); 103 | 104 | startUserPlayerBtn.addEventListener('click', () => { 105 | conn.send({cmd: "player start"}) 106 | startUserPlayerBtn.setAttribute('disabled', true); 107 | stopUserPlayerBtn.removeAttribute('disabled'); 108 | pauseUserPlayerBtn.removeAttribute('disabled'); 109 | rewind.removeAttribute('disabled'); 110 | fforward.removeAttribute('disabled'); 111 | }); 112 | pauseUserPlayerBtn.addEventListener('click', () => { 113 | conn.send({cmd: "player pause"}); 114 | pauseUserPlayerBtn.setAttribute('disabled', true); 115 | startUserPlayerBtn.removeAttribute('disabled'); 116 | }); 117 | stopUserPlayerBtn.addEventListener('click', () => { 118 | conn.send({cmd: "player stop"}); 119 | stopUserPlayerBtn.setAttribute('disabled', true); 120 | pauseUserPlayerBtn.setAttribute('disabled', true); 121 | startUserPlayerBtn.removeAttribute('disabled'); 122 | }); 123 | rewind.addEventListener('click', () => { 124 | conn.send({cmd: "rewind"}); 125 | }); 126 | fforward.addEventListener('click', () => { 127 | conn.send({cmd: "fforward"}); 128 | }) 129 | }); 130 | 131 | }) 132 | const disableTransportButtons = () => { 133 | startUserPlayerBtn.setAttribute('disabled', true); 134 | stopUserPlayerBtn.setAttribute('disabled', true); 135 | rewind.setAttribute('diabled', true); 136 | fforward.setAttribute('diabled', true); 137 | } 138 | startCtx.addEventListener('click', () => { 139 | startCtx.setAttribute('disabled', true); 140 | disconnectButton.removeAttribute('disabled'); 141 | setupAdminMic(); 142 | }); 143 | 144 | 145 | 146 | const setupAdminMic = async () => { 147 | ctx = await new AudioContext(); 148 | navigator.mediaDevices.getUserMedia({audio: true}) 149 | .then((micStream) => { 150 | outputToUser = ctx.createMediaStreamDestination(); 151 | let micSource = ctx.createMediaStreamSource(micStream); 152 | adminMicGain = ctx.createGain(); 153 | adminMicGain.gain.value = 0; 154 | micSource.connect(adminMicGain); 155 | adminMicGain.connect(outputToUser); 156 | callToUser() 157 | }) 158 | .catch(err => console.error(err)) 159 | } 160 | 161 | const callToUser = () => { 162 | let call = peer.call('user', outputToUser.stream); 163 | disconnectButton.addEventListener('click', () => { 164 | startCtx.removeAttribute('disabled'); 165 | disconnectButton.setAttribute('disabled', true); 166 | 167 | // ctx.close(); 168 | ctx = null; 169 | disableTransportButtons(); 170 | call.close(); 171 | }); 172 | call.on('stream', stream => { 173 | console.log("Got Stream...") 174 | adminPlayer.srcObject = stream; 175 | adminPlayer.play(); 176 | }) 177 | call.on('error', (err) => { 178 | console.log(err); 179 | }) 180 | } 181 | 182 | -------------------------------------------------------------------------------- /server/public/fav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CannonFodderr/audiosystem/9ec1c03fe8cc2717e1ab22850f9b74c202e185dd/server/public/fav.png -------------------------------------------------------------------------------- /server/public/pitchFinder.js: -------------------------------------------------------------------------------- 1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0 && arguments[0] !== undefined ? arguments[0] : {}; 21 | 22 | 23 | var sampleRate = config.sampleRate || DEFAULT_SAMPLE_RATE; 24 | var minFrequency = config.minFrequency || DEFAULT_MIN_FREQUENCY; 25 | var maxFrequency = config.maxFrequency || DEFAULT_MAX_FREQUENCY; 26 | var sensitivity = config.sensitivity || DEFAULT_SENSITIVITY; 27 | var ratio = config.ratio || DEFAULT_RATIO; 28 | var amd = []; 29 | var maxPeriod = Math.round(sampleRate / minFrequency + 0.5); 30 | var minPeriod = Math.round(sampleRate / maxFrequency + 0.5); 31 | 32 | return function AMDFDetector(float32AudioBuffer) { 33 | "use strict"; 34 | 35 | var maxShift = float32AudioBuffer.length; 36 | 37 | var t = 0; 38 | var minval = Infinity; 39 | var maxval = -Infinity; 40 | var frames1 = void 0, 41 | frames2 = void 0, 42 | calcSub = void 0, 43 | i = void 0, 44 | j = void 0, 45 | u = void 0, 46 | aux1 = void 0, 47 | aux2 = void 0; 48 | 49 | // Find the average magnitude difference for each possible period offset. 50 | for (i = 0; i < maxShift; i++) { 51 | if (minPeriod <= i && i <= maxPeriod) { 52 | for (aux1 = 0, aux2 = i, t = 0, frames1 = [], frames2 = []; aux1 < maxShift - i; t++, aux2++, aux1++) { 53 | frames1[t] = float32AudioBuffer[aux1]; 54 | frames2[t] = float32AudioBuffer[aux2]; 55 | } 56 | 57 | // Take the difference between these frames. 58 | var frameLength = frames1.length; 59 | calcSub = []; 60 | for (u = 0; u < frameLength; u++) { 61 | calcSub[u] = frames1[u] - frames2[u]; 62 | } 63 | 64 | // Sum the differences. 65 | var summation = 0; 66 | for (u = 0; u < frameLength; u++) { 67 | summation += Math.abs(calcSub[u]); 68 | } 69 | amd[i] = summation; 70 | } 71 | } 72 | 73 | for (j = minPeriod; j < maxPeriod; j++) { 74 | if (amd[j] < minval) minval = amd[j]; 75 | if (amd[j] > maxval) maxval = amd[j]; 76 | } 77 | 78 | var cutoff = Math.round(sensitivity * (maxval - minval) + minval); 79 | for (j = minPeriod; j <= maxPeriod && amd[j] > cutoff; j++) {} 80 | 81 | var search_length = minPeriod / 2; 82 | minval = amd[j]; 83 | var minpos = j; 84 | for (i = j - 1; i < j + search_length && i <= maxPeriod; i++) { 85 | if (amd[i] < minval) { 86 | minval = amd[i]; 87 | minpos = i; 88 | } 89 | } 90 | 91 | if (Math.round(amd[minpos] * ratio) < maxval) { 92 | return sampleRate / minpos; 93 | } else { 94 | return null; 95 | } 96 | }; 97 | }; 98 | },{}],4:[function(require,module,exports){ 99 | "use strict"; 100 | 101 | var DEFAULT_SAMPLE_RATE = 44100; 102 | var MAX_FLWT_LEVELS = 6; 103 | var MAX_F = 3000; 104 | var DIFFERENCE_LEVELS_N = 3; 105 | var MAXIMA_THRESHOLD_RATIO = 0.75; 106 | 107 | module.exports = function () { 108 | var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 109 | 110 | 111 | var sampleRate = config.sampleRate || DEFAULT_SAMPLE_RATE; 112 | 113 | return function DynamicWaveletDetector(float32AudioBuffer) { 114 | "use strict"; 115 | 116 | var mins = []; 117 | var maxs = []; 118 | var bufferLength = float32AudioBuffer.length; 119 | 120 | var freq = null; 121 | var theDC = 0; 122 | var minValue = 0; 123 | var maxValue = 0; 124 | 125 | // Compute max amplitude, amplitude threshold, and the DC. 126 | for (var i = 0; i < bufferLength; i++) { 127 | var sample = float32AudioBuffer[i]; 128 | theDC = theDC + sample; 129 | maxValue = Math.max(maxValue, sample); 130 | minValue = Math.min(minValue, sample); 131 | } 132 | 133 | theDC /= bufferLength; 134 | minValue -= theDC; 135 | maxValue -= theDC; 136 | var amplitudeMax = maxValue > -1 * minValue ? maxValue : -1 * minValue; 137 | var amplitudeThreshold = amplitudeMax * MAXIMA_THRESHOLD_RATIO; 138 | 139 | // levels, start without downsampling... 140 | var curLevel = 0; 141 | var curModeDistance = -1; 142 | var curSamNb = float32AudioBuffer.length; 143 | var delta = void 0, 144 | nbMaxs = void 0, 145 | nbMins = void 0; 146 | 147 | // Search: 148 | while (true) { 149 | delta = ~~(sampleRate / (Math.pow(2, curLevel) * MAX_F)); 150 | if (curSamNb < 2) break; 151 | 152 | var dv = void 0; 153 | var previousDV = -1000; 154 | var lastMinIndex = -1000000; 155 | var lastMaxIndex = -1000000; 156 | var findMax = false; 157 | var findMin = false; 158 | 159 | nbMins = 0; 160 | nbMaxs = 0; 161 | 162 | for (var _i = 2; _i < curSamNb; _i++) { 163 | var si = float32AudioBuffer[_i] - theDC; 164 | var si1 = float32AudioBuffer[_i - 1] - theDC; 165 | 166 | if (si1 <= 0 && si > 0) findMax = true; 167 | if (si1 >= 0 && si < 0) findMin = true; 168 | 169 | // min or max ? 170 | dv = si - si1; 171 | 172 | if (previousDV > -1000) { 173 | if (findMin && previousDV < 0 && dv >= 0) { 174 | // minimum 175 | if (Math.abs(si) >= amplitudeThreshold) { 176 | if (_i > lastMinIndex + delta) { 177 | mins[nbMins++] = _i; 178 | lastMinIndex = _i; 179 | findMin = false; 180 | } 181 | } 182 | } 183 | 184 | if (findMax && previousDV > 0 && dv <= 0) { 185 | // maximum 186 | if (Math.abs(si) >= amplitudeThreshold) { 187 | if (_i > lastMaxIndex + delta) { 188 | maxs[nbMaxs++] = _i; 189 | lastMaxIndex = _i; 190 | findMax = false; 191 | } 192 | } 193 | } 194 | } 195 | previousDV = dv; 196 | } 197 | 198 | if (nbMins === 0 && nbMaxs === 0) { 199 | // No best distance found! 200 | break; 201 | } 202 | 203 | var d = void 0; 204 | var distances = []; 205 | 206 | for (var _i2 = 0; _i2 < curSamNb; _i2++) { 207 | distances[_i2] = 0; 208 | } 209 | 210 | for (var _i3 = 0; _i3 < nbMins; _i3++) { 211 | for (var j = 1; j < DIFFERENCE_LEVELS_N; j++) { 212 | if (_i3 + j < nbMins) { 213 | d = Math.abs(mins[_i3] - mins[_i3 + j]); 214 | distances[d] += 1; 215 | } 216 | } 217 | } 218 | 219 | var bestDistance = -1; 220 | var bestValue = -1; 221 | 222 | for (var _i4 = 0; _i4 < curSamNb; _i4++) { 223 | var summed = 0; 224 | for (var _j = -1 * delta; _j <= delta; _j++) { 225 | if (_i4 + _j >= 0 && _i4 + _j < curSamNb) { 226 | summed += distances[_i4 + _j]; 227 | } 228 | } 229 | 230 | if (summed === bestValue) { 231 | if (_i4 === 2 * bestDistance) { 232 | bestDistance = _i4; 233 | } 234 | } else if (summed > bestValue) { 235 | bestValue = summed; 236 | bestDistance = _i4; 237 | } 238 | } 239 | 240 | // averaging 241 | var distAvg = 0; 242 | var nbDists = 0; 243 | for (var _j2 = -delta; _j2 <= delta; _j2++) { 244 | if (bestDistance + _j2 >= 0 && bestDistance + _j2 < bufferLength) { 245 | var nbDist = distances[bestDistance + _j2]; 246 | if (nbDist > 0) { 247 | nbDists += nbDist; 248 | distAvg += (bestDistance + _j2) * nbDist; 249 | } 250 | } 251 | } 252 | 253 | // This is our mode distance. 254 | distAvg /= nbDists; 255 | 256 | // Continue the levels? 257 | if (curModeDistance > -1) { 258 | if (Math.abs(distAvg * 2 - curModeDistance) <= 2 * delta) { 259 | // two consecutive similar mode distances : ok ! 260 | freq = sampleRate / (Math.pow(2, curLevel - 1) * curModeDistance); 261 | break; 262 | } 263 | } 264 | 265 | // not similar, continue next level; 266 | curModeDistance = distAvg; 267 | 268 | curLevel++; 269 | if (curLevel >= MAX_FLWT_LEVELS || curSamNb < 2) { 270 | break; 271 | } 272 | 273 | //do not modify original audio buffer, make a copy buffer, if 274 | //downsampling is needed (only once). 275 | var newFloat32AudioBuffer = float32AudioBuffer.subarray(0); 276 | if (curSamNb === distances.length) { 277 | newFloat32AudioBuffer = new Float32Array(curSamNb / 2); 278 | } 279 | for (var _i5 = 0; _i5 < curSamNb / 2; _i5++) { 280 | newFloat32AudioBuffer[_i5] = (float32AudioBuffer[2 * _i5] + float32AudioBuffer[2 * _i5 + 1]) / 2; 281 | } 282 | float32AudioBuffer = newFloat32AudioBuffer; 283 | curSamNb /= 2; 284 | } 285 | 286 | return freq; 287 | }; 288 | }; 289 | },{}],5:[function(require,module,exports){ 290 | "use strict"; 291 | 292 | module.exports = function (config) { 293 | 294 | config = config || {}; 295 | 296 | /** 297 | * The expected size of an audio buffer (in samples). 298 | */ 299 | var DEFAULT_BUFFER_SIZE = 1024; 300 | 301 | /** 302 | * Defines the relative size the chosen peak (pitch) has. 0.93 means: choose 303 | * the first peak that is higher than 93% of the highest peak detected. 93% 304 | * is the default value used in the Tartini user interface. 305 | */ 306 | var DEFAULT_CUTOFF = 0.97; 307 | 308 | var DEFAULT_SAMPLE_RATE = 44100; 309 | 310 | /** 311 | * For performance reasons, peaks below this cutoff are not even considered. 312 | */ 313 | var SMALL_CUTOFF = 0.5; 314 | 315 | /** 316 | * Pitch annotations below this threshold are considered invalid, they are 317 | * ignored. 318 | */ 319 | var LOWER_PITCH_CUTOFF = 80; 320 | 321 | /** 322 | * Defines the relative size the chosen peak (pitch) has. 323 | */ 324 | var cutoff = config.cutoff || DEFAULT_CUTOFF; 325 | 326 | /** 327 | * The audio sample rate. Most audio has a sample rate of 44.1kHz. 328 | */ 329 | var sampleRate = config.sampleRate || DEFAULT_SAMPLE_RATE; 330 | 331 | /** 332 | * Size of the input buffer. 333 | */ 334 | var bufferSize = config.bufferSize || DEFAULT_BUFFER_SIZE; 335 | 336 | /** 337 | * Contains a normalized square difference function value for each delay 338 | * (tau). 339 | */ 340 | var nsdf = new Float32Array(bufferSize); 341 | 342 | /** 343 | * The x and y coordinate of the top of the curve (nsdf). 344 | */ 345 | var turningPointX = void 0; 346 | var turningPointY = void 0; 347 | 348 | /** 349 | * A list with minimum and maximum values of the nsdf curve. 350 | */ 351 | var maxPositions = []; 352 | 353 | /** 354 | * A list of estimates of the period of the signal (in samples). 355 | */ 356 | var periodEstimates = []; 357 | 358 | /** 359 | * A list of estimates of the amplitudes corresponding with the period 360 | * estimates. 361 | */ 362 | var ampEstimates = []; 363 | 364 | /** 365 | * The result of the pitch detection iteration. 366 | */ 367 | var result = {}; 368 | 369 | /** 370 | * Implements the normalized square difference function. See section 4 (and 371 | * the explanation before) in the MPM article. This calculation can be 372 | * optimized by using an FFT. The results should remain the same. 373 | */ 374 | var normalizedSquareDifference = function normalizedSquareDifference(float32AudioBuffer) { 375 | for (var tau = 0; tau < float32AudioBuffer.length; tau++) { 376 | var acf = 0; 377 | var divisorM = 0; 378 | for (var i = 0; i < float32AudioBuffer.length - tau; i++) { 379 | acf += float32AudioBuffer[i] * float32AudioBuffer[i + tau]; 380 | divisorM += float32AudioBuffer[i] * float32AudioBuffer[i] + float32AudioBuffer[i + tau] * float32AudioBuffer[i + tau]; 381 | } 382 | nsdf[tau] = 2 * acf / divisorM; 383 | } 384 | }; 385 | 386 | /** 387 | * Finds the x value corresponding with the peak of a parabola. 388 | * Interpolates between three consecutive points centered on tau. 389 | */ 390 | var parabolicInterpolation = function parabolicInterpolation(tau) { 391 | var nsdfa = nsdf[tau - 1], 392 | nsdfb = nsdf[tau], 393 | nsdfc = nsdf[tau + 1], 394 | bValue = tau, 395 | bottom = nsdfc + nsdfa - 2 * nsdfb; 396 | if (bottom === 0) { 397 | turningPointX = bValue; 398 | turningPointY = nsdfb; 399 | } else { 400 | var delta = nsdfa - nsdfc; 401 | turningPointX = bValue + delta / (2 * bottom); 402 | turningPointY = nsdfb - delta * delta / (8 * bottom); 403 | } 404 | }; 405 | 406 | // Finds the highest value between each pair of positive zero crossings. 407 | var peakPicking = function peakPicking() { 408 | var pos = 0; 409 | var curMaxPos = 0; 410 | 411 | // find the first negative zero crossing. 412 | while (pos < (nsdf.length - 1) / 3 && nsdf[pos] > 0) { 413 | pos++; 414 | } 415 | 416 | // loop over all the values below zero. 417 | while (pos < nsdf.length - 1 && nsdf[pos] <= 0) { 418 | pos++; 419 | } 420 | 421 | // can happen if output[0] is NAN 422 | if (pos == 0) { 423 | pos = 1; 424 | } 425 | 426 | while (pos < nsdf.length - 1) { 427 | if (nsdf[pos] > nsdf[pos - 1] && nsdf[pos] >= nsdf[pos + 1]) { 428 | if (curMaxPos == 0) { 429 | // the first max (between zero crossings) 430 | curMaxPos = pos; 431 | } else if (nsdf[pos] > nsdf[curMaxPos]) { 432 | // a higher max (between the zero crossings) 433 | curMaxPos = pos; 434 | } 435 | } 436 | pos++; 437 | // a negative zero crossing 438 | if (pos < nsdf.length - 1 && nsdf[pos] <= 0) { 439 | // if there was a maximum add it to the list of maxima 440 | if (curMaxPos > 0) { 441 | maxPositions.push(curMaxPos); 442 | curMaxPos = 0; // clear the maximum position, so we start 443 | // looking for a new ones 444 | } 445 | while (pos < nsdf.length - 1 && nsdf[pos] <= 0) { 446 | pos++; // loop over all the values below zero 447 | } 448 | } 449 | } 450 | if (curMaxPos > 0) { 451 | maxPositions.push(curMaxPos); 452 | } 453 | }; 454 | 455 | return function (float32AudioBuffer) { 456 | 457 | // 0. Clear old results. 458 | var pitch = void 0; 459 | maxPositions = []; 460 | periodEstimates = []; 461 | ampEstimates = []; 462 | 463 | // 1. Calculute the normalized square difference for each Tau value. 464 | normalizedSquareDifference(float32AudioBuffer); 465 | // 2. Peak picking time: time to pick some peaks. 466 | peakPicking(); 467 | 468 | var highestAmplitude = -Infinity; 469 | 470 | for (var i = 0; i < maxPositions.length; i++) { 471 | var tau = maxPositions[i]; 472 | // make sure every annotation has a probability attached 473 | highestAmplitude = Math.max(highestAmplitude, nsdf[tau]); 474 | 475 | if (nsdf[tau] > SMALL_CUTOFF) { 476 | // calculates turningPointX and Y 477 | parabolicInterpolation(tau); 478 | // store the turning points 479 | ampEstimates.push(turningPointY); 480 | periodEstimates.push(turningPointX); 481 | // remember the highest amplitude 482 | highestAmplitude = Math.max(highestAmplitude, turningPointY); 483 | } 484 | } 485 | 486 | if (periodEstimates.length) { 487 | // use the overall maximum to calculate a cutoff. 488 | // The cutoff value is based on the highest value and a relative 489 | // threshold. 490 | var actualCutoff = cutoff * highestAmplitude; 491 | var periodIndex = 0; 492 | 493 | for (var _i = 0; _i < ampEstimates.length; _i++) { 494 | if (ampEstimates[_i] >= actualCutoff) { 495 | periodIndex = _i; 496 | break; 497 | } 498 | } 499 | 500 | var period = periodEstimates[periodIndex], 501 | pitchEstimate = sampleRate / period; 502 | 503 | if (pitchEstimate > LOWER_PITCH_CUTOFF) { 504 | pitch = pitchEstimate; 505 | } else { 506 | pitch = -1; 507 | } 508 | } else { 509 | // no pitch detected. 510 | pitch = -1; 511 | } 512 | 513 | result.probability = highestAmplitude; 514 | result.freq = pitch; 515 | return result; 516 | }; 517 | }; 518 | },{}],6:[function(require,module,exports){ 519 | "use strict"; 520 | 521 | /* 522 | Copyright (C) 2003-2009 Paul Brossier 523 | This file is part of aubio. 524 | aubio is free software: you can redistribute it and/or modify 525 | it under the terms of the GNU General Public License as published by 526 | the Free Software Foundation, either version 3 of the License, or 527 | (at your option) any later version. 528 | aubio is distributed in the hope that it will be useful, 529 | but WITHOUT ANY WARRANTY; without even the implied warranty of 530 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 531 | GNU General Public License for more details. 532 | You should have received a copy of the GNU General Public License 533 | along with aubio. If not, see . 534 | */ 535 | 536 | /* This algorithm was developed by A. de Cheveigné and H. Kawahara and 537 | * published in: 538 | * 539 | * de Cheveigné, A., Kawahara, H. (2002) "YIN, a fundamental frequency 540 | * estimator for speech and music", J. Acoust. Soc. Am. 111, 1917-1930. 541 | * 542 | * see http://recherche.ircam.fr/equipes/pcm/pub/people/cheveign.html 543 | */ 544 | 545 | var DEFAULT_THRESHOLD = 0.10; 546 | var DEFAULT_SAMPLE_RATE = 44100; 547 | var DEFAULT_PROBABILITY_THRESHOLD = 0.1; 548 | 549 | module.exports = function () { 550 | var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 551 | 552 | 553 | var threshold = config.threshold || DEFAULT_THRESHOLD; 554 | var sampleRate = config.sampleRate || DEFAULT_SAMPLE_RATE; 555 | var probabilityThreshold = config.probabilityThreshold || DEFAULT_PROBABILITY_THRESHOLD; 556 | 557 | return function YINDetector(float32AudioBuffer) { 558 | "use strict"; 559 | 560 | // Set buffer size to the highest power of two below the provided buffer's length. 561 | 562 | var bufferSize = void 0; 563 | for (bufferSize = 1; bufferSize < float32AudioBuffer.length; bufferSize *= 2) {} 564 | bufferSize /= 2; 565 | 566 | // Set up the yinBuffer as described in step one of the YIN paper. 567 | var yinBufferLength = bufferSize / 2; 568 | var yinBuffer = new Float32Array(yinBufferLength); 569 | 570 | var probability = void 0, 571 | tau = void 0; 572 | 573 | // Compute the difference function as described in step 2 of the YIN paper. 574 | for (var t = 0; t < yinBufferLength; t++) { 575 | yinBuffer[t] = 0; 576 | } 577 | for (var _t = 1; _t < yinBufferLength; _t++) { 578 | for (var i = 0; i < yinBufferLength; i++) { 579 | var delta = float32AudioBuffer[i] - float32AudioBuffer[i + _t]; 580 | yinBuffer[_t] += delta * delta; 581 | } 582 | } 583 | 584 | // Compute the cumulative mean normalized difference as described in step 3 of the paper. 585 | yinBuffer[0] = 1; 586 | yinBuffer[1] = 1; 587 | var runningSum = 0; 588 | for (var _t2 = 1; _t2 < yinBufferLength; _t2++) { 589 | runningSum += yinBuffer[_t2]; 590 | yinBuffer[_t2] *= _t2 / runningSum; 591 | } 592 | 593 | // Compute the absolute threshold as described in step 4 of the paper. 594 | // Since the first two positions in the array are 1, 595 | // we can start at the third position. 596 | for (tau = 2; tau < yinBufferLength; tau++) { 597 | if (yinBuffer[tau] < threshold) { 598 | while (tau + 1 < yinBufferLength && yinBuffer[tau + 1] < yinBuffer[tau]) { 599 | tau++; 600 | } 601 | // found tau, exit loop and return 602 | // store the probability 603 | // From the YIN paper: The threshold determines the list of 604 | // candidates admitted to the set, and can be interpreted as the 605 | // proportion of aperiodic power tolerated 606 | // within a periodic signal. 607 | // 608 | // Since we want the periodicity and and not aperiodicity: 609 | // periodicity = 1 - aperiodicity 610 | probability = 1 - yinBuffer[tau]; 611 | break; 612 | } 613 | } 614 | 615 | // if no pitch found, return null. 616 | if (tau == yinBufferLength || yinBuffer[tau] >= threshold) { 617 | return null; 618 | } 619 | 620 | // If probability too low, return -1. 621 | if (probability < probabilityThreshold) { 622 | return null; 623 | } 624 | 625 | /** 626 | * Implements step 5 of the AUBIO_YIN paper. It refines the estimated tau 627 | * value using parabolic interpolation. This is needed to detect higher 628 | * frequencies more precisely. See http://fizyka.umk.pl/nrbook/c10-2.pdf and 629 | * for more background 630 | * http://fedc.wiwi.hu-berlin.de/xplore/tutorials/xegbohtmlnode62.html 631 | */ 632 | var betterTau = void 0, 633 | x0 = void 0, 634 | x2 = void 0; 635 | if (tau < 1) { 636 | x0 = tau; 637 | } else { 638 | x0 = tau - 1; 639 | } 640 | if (tau + 1 < yinBufferLength) { 641 | x2 = tau + 1; 642 | } else { 643 | x2 = tau; 644 | } 645 | if (x0 === tau) { 646 | if (yinBuffer[tau] <= yinBuffer[x2]) { 647 | betterTau = tau; 648 | } else { 649 | betterTau = x2; 650 | } 651 | } else if (x2 === tau) { 652 | if (yinBuffer[tau] <= yinBuffer[x0]) { 653 | betterTau = tau; 654 | } else { 655 | betterTau = x0; 656 | } 657 | } else { 658 | var s0 = yinBuffer[x0]; 659 | var s1 = yinBuffer[tau]; 660 | var s2 = yinBuffer[x2]; 661 | // fixed AUBIO implementation, thanks to Karl Helgason: 662 | // (2.0f * s1 - s2 - s0) was incorrectly multiplied with -1 663 | betterTau = tau + (s2 - s0) / (2 * (2 * s1 - s2 - s0)); 664 | } 665 | 666 | return sampleRate / betterTau; 667 | }; 668 | }; 669 | },{}],7:[function(require,module,exports){ 670 | "use strict"; 671 | 672 | var AMDF = require("./detectors/amdf"); 673 | var YIN = require("./detectors/yin"); 674 | var DynamicWavelet = require("./detectors/dynamic_wavelet"); 675 | var Macleod = require("./detectors/macleod"); 676 | 677 | var frequencies = require("./tools/frequencies"); 678 | 679 | module.exports = { 680 | AMDF: AMDF, 681 | YIN: YIN, 682 | DynamicWavelet: DynamicWavelet, 683 | Macleod: Macleod, 684 | frequencies: frequencies 685 | }; 686 | },{"./detectors/amdf":3,"./detectors/dynamic_wavelet":4,"./detectors/macleod":5,"./detectors/yin":6,"./tools/frequencies":8}],8:[function(require,module,exports){ 687 | "use strict"; 688 | 689 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 690 | 691 | var DEFAULT_TEMPO = 120; 692 | var DEFAULT_QUANTIZATION = 4; 693 | var DEFAULT_SAMPLE_RATE = 44100; 694 | 695 | function pitchConsensus(detectors, chunk) { 696 | var pitches = detectors.map(function (fn) { 697 | return fn(chunk); 698 | }).filter(Boolean).sort(function (a, b) { 699 | return a < b ? -1 : 1; 700 | }); 701 | 702 | // In the case of one pitch, return it. 703 | if (pitches.length === 1) { 704 | return pitches[0]; 705 | 706 | // In the case of two pitches, return the geometric mean if they 707 | // are close to each other, and the lower pitch otherwise. 708 | } else if (pitches.length === 2) { 709 | var _pitches = _slicedToArray(pitches, 2), 710 | first = _pitches[0], 711 | second = _pitches[1]; 712 | 713 | return first * 2 > second ? Math.sqrt(first * second) : first; 714 | 715 | // In the case of three or more pitches, filter away the extremes 716 | // if they are very extreme, then take the geometric mean. 717 | } else { 718 | var _first = pitches[0]; 719 | var _second = pitches[1]; 720 | var secondToLast = pitches[pitches.length - 2]; 721 | var last = pitches[pitches.length - 1]; 722 | 723 | var filtered1 = _first * 2 > _second ? pitches : pitches.slice(1); 724 | var filtered2 = secondToLast * 2 > last ? filtered1 : filtered1.slice(0, -1); 725 | return Math.pow(filtered2.reduce(function (t, p) { 726 | return t * p; 727 | }, 1), 1 / filtered2.length); 728 | } 729 | } 730 | 731 | module.exports = function (detector, float32AudioBuffer) { 732 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 733 | 734 | 735 | var tempo = options.tempo || DEFAULT_TEMPO; 736 | var quantization = options.quantization || DEFAULT_QUANTIZATION; 737 | var sampleRate = options.sampleRate || DEFAULT_SAMPLE_RATE; 738 | 739 | var bufferLength = float32AudioBuffer.length; 740 | var chunkSize = Math.round(sampleRate * 60 / (quantization * tempo)); 741 | 742 | var getPitch = void 0; 743 | if (Array.isArray(detector)) { 744 | getPitch = pitchConsensus.bind(null, detector); 745 | } else { 746 | getPitch = detector; 747 | } 748 | 749 | var pitches = []; 750 | for (var i = 0, max = bufferLength - chunkSize; i <= max; i += chunkSize) { 751 | var chunk = float32AudioBuffer.slice(i, i + chunkSize); 752 | var pitch = getPitch(chunk); 753 | pitches.push(pitch); 754 | } 755 | 756 | return pitches; 757 | }; 758 | },{}]},{},[1]); 759 | -------------------------------------------------------------------------------- /server/public/script.js: -------------------------------------------------------------------------------- 1 | // alert("Connected") 2 | -------------------------------------------------------------------------------- /server/public/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | body { 7 | background: #3e3e3e; 8 | color: #fff; 9 | } 10 | 11 | audio{ 12 | width: 100%; 13 | } -------------------------------------------------------------------------------- /server/public/user.js: -------------------------------------------------------------------------------- 1 | // PEER CONNECTION 2 | const roomName = document.getElementById('room-title').innerHTML; 3 | const host = window.location.hostname; 4 | 5 | const peer = new Peer(roomName, { 6 | host: host, 7 | port: '8080', 8 | path: '/peerjs', 9 | sdpSemantics: 'unified-plan', 10 | debug: 0, 11 | }); 12 | 13 | let reconnectAttempts = 0; 14 | let call = null; 15 | let isAdminConnected = false; 16 | let isPlaying = false; 17 | let updateAdminTimer; 18 | // DOM Elements 19 | const startCtxBtn = document.getElementById('startctx'); 20 | const userPlayer = document.querySelector('audio'); 21 | 22 | // AudioContext Setup 23 | let ctx; 24 | let sampleRate; 25 | let micGain; 26 | let playerGain; 27 | let micStream; 28 | 29 | let micToAdminGain; 30 | let playerToAdminGain; 31 | // Streams Variables 32 | let playerStream; 33 | let micStreamToOutput; 34 | let clonedMicStream; 35 | let clonedPlayerStream; 36 | 37 | let micGainValue = 0.5; 38 | let playerGainValue = 0.7; 39 | let analyser; 40 | let dataArray; 41 | let pitchArr = []; 42 | 43 | // Load Pitchfinder 44 | let PitchFinder = window.PitchFinder 45 | let detectPitch = PitchFinder.YIN(); 46 | let isOscActive = true; 47 | let isOscPlaying = false; 48 | let oscGainValue = 0.001; 49 | 50 | const playOsc = pitch => { 51 | let osc = ctx.createOscillator(); 52 | let oscGain = ctx.createGain(); 53 | osc.type = 'sine'; 54 | osc.frequency.setValueAtTime(pitch*100, ctx.currentTime); 55 | oscGain.gain.value = oscGainValue; 56 | osc.connect(oscGain); 57 | oscGain.connect(ctx.destination) 58 | isOscPlaying = true; 59 | setTimeout(() => { isOscPlaying = false}, 1000); 60 | osc.start(); 61 | oscGain.gain.setValueAtTime(0, ctx.currentTime + 0.8) 62 | setTimeout(() => { 63 | osc.stop(); 64 | }, 1000) 65 | } 66 | 67 | const pitchDetector = () => { 68 | if(!isPlaying){ 69 | return; 70 | } 71 | // Populate the dataArray from the analyser method 72 | let dataArray = new Uint8Array(analyser.fftSize) 73 | analyser.getByteTimeDomainData(dataArray); 74 | // Detect pitch and push to array; 75 | let pitch = detectPitch(dataArray, { sampleRate: 48000}); 76 | pitchArr.push(pitch); 77 | if(pitchArr.length > 2){ 78 | pitchArr.shift() 79 | } 80 | // Pervent overloading the oscillator 81 | if(isOscActive && !isOscPlaying && pitchArr[1] && pitchArr[1] !== pitchArr[0]){ 82 | playOsc(pitchArr[1]) 83 | } 84 | requestAnimationFrame(pitchDetector) 85 | } 86 | 87 | peer.on('disconnected', () => { 88 | console.log("Trying to reconnect to server"); 89 | reconnect() 90 | }) 91 | 92 | if(document.readyState === "complete"){ 93 | console.log("Ready") 94 | // Start Audio Context after document is loaded 95 | startCtxBtn.addEventListener('click', () => { 96 | startCtxBtn.setAttribute('disabled', true); 97 | startCtxBtn.style.display = "none"; 98 | initUserPlayback() 99 | }); 100 | } 101 | 102 | 103 | 104 | // Get Stream from audio player 105 | const initUserPlayback = () => { 106 | ctx = new(window.AudioContext || window.webkitAudioContext)(); 107 | // SETUP ANALYSER 108 | sampleRate = ctx.sampleRate; 109 | analyser = ctx.createAnalyser(); 110 | // Capture Stream from audio player 111 | if(isChromeBrowser()){ 112 | playerStream = userPlayer.captureStream(); 113 | console.log("Chrome Capture"); 114 | } else { 115 | playerStream = userPlayer.mozCaptureStream(); 116 | console.log("FireFox Capture"); 117 | } 118 | playerGain = ctx.createGain() 119 | playerGain.gain.value = playerGainValue; 120 | let playerStreamToOutput = ctx.createMediaStreamSource(playerStream); 121 | let filter = ctx.createBiquadFilter(); 122 | filter.type = "highpass"; 123 | filter.frequency.value = 250; 124 | playerStreamToOutput.connect(filter); 125 | filter.connect(analyser); 126 | analyser.connect(playerGain); 127 | playerGain.connect(ctx.destination); 128 | getUserMicStream() 129 | } 130 | 131 | // Get stream from user Microphone 132 | let getUserMicStream = () => { 133 | let constraints = { video: false, audio: true }; 134 | navigator.mediaDevices.getUserMedia(constraints) 135 | .then(stream => { 136 | micStream = stream; 137 | micGain = ctx.createGain() 138 | micGain.gain.value = micGainValue; 139 | micStreamToOutput = ctx.createMediaStreamSource(micStream); 140 | micStreamToOutput.connect(micGain); 141 | micGain.connect(ctx.destination); 142 | setupConnection(); 143 | }) 144 | .catch((err) => console.error(err)); 145 | } 146 | let isChromeBrowser = () => { 147 | if(navigator.userAgent.indexOf("Chrome") >= 0){ 148 | console.log("Chrome") 149 | return true 150 | } else if(navigator.userAgent.indexOf("Firefox") >= 0){ 151 | console.log("FireFox") 152 | return false 153 | } 154 | } 155 | let reconnect = () => { 156 | ctx.close(); 157 | // ctx = null; 158 | setupConnection(); 159 | reconnectAttempts += 1; 160 | console.log(reconnectAttempts); 161 | } 162 | // Setup peer 2 peer connection listen on incoming data; 163 | const setupConnection = () =>{ 164 | console.log(isAdminConnected); 165 | let conn = peer.connect('admin', {serialization: "json"}); 166 | if(!conn){ 167 | console.log(peer.connections); 168 | if(confirm("Unable to connect reload page?")){ 169 | location.reload(); 170 | } 171 | } 172 | conn.on('open', () => { 173 | isAdminConnected = true; 174 | conn.send({cmd: "user online"}); 175 | }); 176 | conn.on('close', () => { 177 | isAdminConnected = false; 178 | console.log("Admin Disconnected") 179 | clearInterval(updateAdminTimer); 180 | if(!isAdminConnected){ 181 | reconnect(); 182 | } 183 | }); 184 | conn.on('data', (data) => { 185 | const updateAdminUi = () => { 186 | conn.send({cmd: "update" ,micGain: micGainValue, playerGain: playerGainValue, isPlaying: isPlaying, playerTime: userPlayer.currentTime, isAdminConnected: isAdminConnected }); 187 | } 188 | if(data.cmd === "admin connect"){ 189 | console.log("Admin Connected"); 190 | updateAdminTimer = setInterval(() => { 191 | updateAdminUi() 192 | }, 1000); 193 | } 194 | if(data.cmd === "mic gain"){ 195 | micGainValue = data.value; 196 | micGain.gain.setValueAtTime(data.value, ctx.currentTime); 197 | micToAdminGain.gain.setValueAtTime(data.value, ctx.currentTime); 198 | } 199 | if(data.cmd === "player gain"){ 200 | playerGainValue = data.value; 201 | playerGain.gain.setValueAtTime(data.value, ctx.currentTime); 202 | playerToAdminGain.gain.setValueAtTime(data.value, ctx.currentTime); 203 | } 204 | if(data.cmd === "osc state"){ 205 | if(data.value === "off"){ 206 | isOscActive = false; 207 | } else { 208 | isOscActive = true; 209 | }; 210 | } 211 | if(data.cmd === "osc gain"){ 212 | oscGainValue = data.value; 213 | } 214 | if(data.cmd === "player start"){ 215 | userPlayer.play(); 216 | isPlaying = true; 217 | pitchDetector(); 218 | } 219 | if(data.cmd === "player pause"){ 220 | isPlaying = false; 221 | userPlayer.pause(); 222 | } 223 | if(data.cmd === "player stop"){ 224 | isPlaying = false; 225 | userPlayer.currentTime = 0; 226 | userPlayer.pause(); 227 | } 228 | if(data.cmd === "rewind"){ 229 | let newTransportPosition = userPlayer.currentTime - 30 > 0 ? userPlayer.currentTime - 30 : 0; 230 | userPlayer.currentTime = newTransportPosition; 231 | } 232 | if(data.cmd === "fforward"){ 233 | let maxDuration = userPlayer.duration; 234 | let newTransportPosition = userPlayer.currentTime + 30 < maxDuration ? userPlayer.currentTime + 30 : maxDuration; 235 | userPlayer.currentTime = newTransportPosition; 236 | } 237 | }); 238 | peer.on('call', (call) => { 239 | console.log("Got call from admin"); 240 | createStreamToAdmin(call) 241 | isAdminConnected = true; 242 | }); 243 | } 244 | 245 | // 1. Clone active AUDIO & MIC Streams 246 | // 2. Create Audio Context sources form cloned streams. 247 | // 3. Create NEW Audio Context destination & connect cloned streams; 248 | // 4 .Peer calls admin with newly created Audio Context destination. 249 | let createStreamToAdmin = async (call) => { 250 | clonedMicStream = micStream.clone(); 251 | clonedPlayerStream = playerStream.clone(); 252 | let streamToAdmin = ctx.createMediaStreamDestination(); 253 | let micStreamToAdmin = ctx.createMediaStreamSource(clonedMicStream); 254 | let playerStreamToAdmin = ctx.createMediaStreamSource(clonedPlayerStream); 255 | // Stream volume mix controls 256 | micToAdminGain = ctx.createGain(); 257 | playerToAdminGain = ctx.createGain(); 258 | // Init mix controls gain 259 | micToAdminGain.gain.setValueAtTime(micGainValue, ctx.currentTime); 260 | playerToAdminGain.gain.setValueAtTime(playerGainValue, ctx.currentTime); 261 | micStreamToAdmin.connect(micToAdminGain); 262 | playerStreamToAdmin.connect(playerToAdminGain) 263 | micToAdminGain.connect(streamToAdmin); 264 | playerToAdminGain.connect(streamToAdmin); 265 | let options = { 266 | 'constraints': { 267 | 'mandatory': { 268 | 'OfferToReceiveAudio': true, 269 | 'OfferToReceiveVideo': false 270 | } 271 | } 272 | } 273 | call.answer(streamToAdmin.stream) 274 | call.on('stream', (adminStream) => { 275 | console.log("Got stream from admin...", adminStream); 276 | let adminMicPlayer = document.getElementById('adminMicPlayer'); 277 | adminMicPlayer.srcObject= adminStream; 278 | adminMicPlayer.play() 279 | console.log(adminMicPlayer); 280 | // let adminTBSource = ctx.createMediaStreamSource(adminStream); 281 | // adminStream.connect(ctx.destination); 282 | }); 283 | } 284 | 285 | 286 | 287 | 288 | 289 | 290 | -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const Room = require('../models/Room'); 3 | const User = require('../models/User'); 4 | const Book = require('../models/Books'); 5 | const {isLoggedIn, isAdmin} = require('../middleware/auth'); 6 | const passport = require('passport'); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const formidable = require('formidable'); 10 | 11 | // AUTHENTICATION 12 | router.post('/login', passport.authenticate('local'), (req, res) => { 13 | if(!req.user){ 14 | return res.json({err: "Username or Password are incorrect"}) 15 | } 16 | return res.json({room: req.user}); 17 | }); 18 | 19 | router.post('/logout', (req, res) => { 20 | res.send("logout"); 21 | }); 22 | 23 | // ROOMS 24 | // Get all rooms 25 | router.get('/rooms', isAdmin, (req, res) => { 26 | Room.find().populate('currentUser').populate('currentBook').exec() 27 | .then((allRooms) => { 28 | res.json(allRooms); 29 | }); 30 | }); 31 | 32 | // Get Room Data 33 | router.get('/rooms/:roomId', isLoggedIn, (req, res) => { 34 | Room.findById(req.params.roomId) 35 | .then(foundRoom => { 36 | res.json(foundRoom); 37 | }) 38 | .catch(err => { 39 | console.log(err) 40 | }) 41 | }); 42 | router.put(`/rooms/:roomId`, isLoggedIn, (req, res) => { 43 | Room.findOneAndUpdate({_id: req.params.roomId}, req.body) 44 | .then(updatedRoom => { 45 | res.json(updatedRoom); 46 | }) 47 | .catch(err => console.log(err)); 48 | }) 49 | 50 | // USER 51 | router.get('/users', isLoggedIn, (req, res) =>{ 52 | User.find() 53 | .then(allUsers => { 54 | res.json(allUsers) 55 | }) 56 | .catch(err => console.log(err)); 57 | }); 58 | 59 | router.post('/users', isLoggedIn, (req, res) => { 60 | let newUser = req.body 61 | User.findOne({email: newUser.email}) 62 | .then((foundUser) => { 63 | if(!foundUser){ 64 | User.create(newUser) 65 | .then(() => {res.send({msg: "Created User"})}) 66 | } else { 67 | res.json({msg: "E-mail already registered"}) 68 | } 69 | }) 70 | .catch((err) => { 71 | console.log(err); 72 | res.json({msg: "Error", data: err}); 73 | }) 74 | }); 75 | 76 | // Find user by id 77 | router.get('/users/:userId',isLoggedIn, (req, res) => { 78 | User.findById(req.params.userId) 79 | .then(foundUser => { 80 | res.json(foundUser) 81 | }) 82 | .catch(err => console.log(err)); 83 | }); 84 | 85 | router.delete('/users/:userId',isLoggedIn, (req, res) => { 86 | User.findOneAndDelete({_id: req.params.userId}).then((deletedUser) => { 87 | console.log("Deleted:" ,deletedUser); 88 | res.json({msg: "Deleted User", data: deletedUser}) 89 | }) 90 | .catch(() => { 91 | console.log(err); 92 | res.json({msg: "Error", data: err}) 93 | }) 94 | }); 95 | 96 | // BOOKS 97 | router.get('/books', isLoggedIn, (req, res) =>{ 98 | Book.find() 99 | .then(allBooks => { 100 | res.json(allBooks) 101 | }) 102 | .catch(err => console.log(err)); 103 | }); 104 | 105 | router.post('/books', isLoggedIn, (req, res) => { 106 | let bookData = { parts: []} 107 | let newBookFolder; 108 | let progress = 0; 109 | let form = new formidable.IncomingForm() 110 | let assetsFolder = path.join(__dirname, "../assets/") 111 | if(!fs.existsSync(assetsFolder)) fs.mkdirSync(assetsFolder); 112 | let booksFolder = path.join(assetsFolder, '/books'); 113 | if(!fs.existsSync(booksFolder)) fs.mkdirSync(booksFolder); 114 | let tempFolder = path.join(assetsFolder, "/books/temp"); 115 | if(!fs.existsSync(tempFolder)) fs.mkdirSync(tempFolder); 116 | form.uploadDir = tempFolder; 117 | form.maxFileSize = 2000 * 1024 * 1024; 118 | form.multiples = true; 119 | form.parse(req, (err, fields, files) => { 120 | newBookFolder = path.join(booksFolder, fields.name) 121 | }); 122 | form.on('field', function(field, value) { 123 | bookData[field] = value; 124 | }); 125 | form.on('fileBegin', (name, file) => { 126 | console.log("Uploading: ", name) 127 | 128 | }); 129 | form.on('error', (err) => { 130 | console.log(err); 131 | }); 132 | form.on('aborted', () => { 133 | console.log("Aborted"); 134 | }) 135 | form.on('file', function(field, file) { 136 | let fileType = file.name.split('.').pop(); 137 | if(fileType === "mp3" || fileType === "wav"){ 138 | fs.renameSync(file.path, form.uploadDir + "/" + file.name); 139 | bookData.parts.push(file.name); 140 | } else { 141 | fs.unlinkSync(file.path) 142 | console.log("Not an audio file: ", file.name); 143 | } 144 | }) 145 | form.on('end', function() { 146 | console.log("Form ended") 147 | if(bookData.parts.length < 1){ 148 | fs.rmdirSync(tempFolder); 149 | res.json({msg: "No files uploaded"}) 150 | } else { 151 | fs.renameSync(tempFolder, newBookFolder) 152 | Book.create(bookData) 153 | .then(createdBook => { 154 | res.send({msg: "Created book", data: createdBook}); 155 | }) 156 | .catch(err => { 157 | console.log(err); 158 | res.send({msg: "Error", err}); 159 | }) 160 | } 161 | }); 162 | 163 | }) 164 | 165 | 166 | // Find book by id 167 | router.get('/books/:bookId', (req, res) => { 168 | Book.findById(req.params.userId) 169 | .then(foundBook => { 170 | res.json(foundBook) 171 | }) 172 | .catch(err => console.log(err)); 173 | }); 174 | 175 | router.delete('/books/:bookId',isLoggedIn, (req, res) => { 176 | Book.findOneAndDelete({_id: req.params.bookId}).then((deletedBook) => { 177 | let deleteFolerPath = path.join(__dirname, '../assets/books/', deletedBook.name) 178 | if(fs.existsSync(deleteFolerPath)){ 179 | let files = fs.readdirSync(deleteFolerPath) 180 | files.forEach(file => { 181 | fs.unlinkSync(path.join(deleteFolerPath, file)) 182 | }); 183 | fs.rmdirSync(deleteFolerPath); 184 | } 185 | res.json({msg: "Deleted Book", data: deletedBook}) 186 | }) 187 | .catch(err => { 188 | console.log(err); 189 | res.json({msg: "Error", data: err}) 190 | }) 191 | }); 192 | 193 | 194 | router.get('/audio', (req, res) => { 195 | console.log("Get file request from:", req.user); 196 | Room.findById(req.user._id).populate('currentBook').populate('currentUser').exec().then((foundRoom) => { 197 | if(!foundRoom || !foundRoom.currentBook.name || !foundRoom.currentPart){ 198 | console.log("Missing path data") 199 | res.send("missin data") 200 | } else { 201 | // SEND FILE TO ROOM 202 | const bookPath = path.join(__dirname, `../assets/books/${foundRoom.currentBook.name}/${foundRoom.currentPart}`); 203 | console.log(bookPath) 204 | const state = fs.statSync(bookPath); 205 | const fileSize = state.size; 206 | const range = req.headers.range; 207 | if(range){ 208 | const parts = range.replace(/bytes=/, "").split("-"); 209 | const start = parseInt(parts[0], 10); 210 | const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; 211 | const chunkSize = (end-start)+1; 212 | const file = fs.createReadStream(bookPath, {start, end}); 213 | const head = { 214 | 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 215 | 'Accept-Ranges': 'bytes', 216 | 'Content-Length': chunkSize, 217 | 'Content-Type': 'audio/mp3', 218 | } 219 | console.log("Streaming...") 220 | res.writeHead(206, head); 221 | file.pipe(res); 222 | }else{ 223 | const head = { 224 | 'Content-Length': fileSize, 225 | 'Content-Type': 'audio/mp3', 226 | } 227 | console.log("Streaming first part") 228 | res.writeHead(200, head) 229 | fs.createReadStream(bookPath).pipe(res) 230 | } 231 | } 232 | }) 233 | 234 | }); 235 | 236 | module.exports = router; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const env = require('dotenv').config(); 2 | const express = require('express'); 3 | const https = require('https'); 4 | const bodyParser = require('body-parser'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const ExpressPeerServer = require('peer').ExpressPeerServer; 8 | const passport = require('passport'); 9 | const LocalStrategy = require('passport-local').Strategy; 10 | const app = express(); 11 | const cors = require('cors'); 12 | 13 | const apiRoutes = require('./routes/api'); 14 | 15 | app.set('view engine', 'ejs'); 16 | 17 | app.use(express.static(path.join(__dirname, 'public'))); 18 | app.use(express.static(path.resolve(__dirname, '../', 'build'))) 19 | app.use(express.json()); 20 | app.use(bodyParser.urlencoded({extended: true})); 21 | app.use(cors()); 22 | 23 | app.use(require('express-session')({ 24 | secret: 'kornishon', 25 | resave: false, 26 | saveUninitialized: false 27 | })); 28 | 29 | app.use(passport.initialize()); 30 | app.use(passport.session()); 31 | 32 | let dbConnection = require('./db/connect'); 33 | 34 | // CONFIG PASSPORT 35 | const Room = require('./models/Room'); 36 | passport.use(new LocalStrategy(Room.authenticate())); 37 | passport.serializeUser(Room.serializeUser()); 38 | passport.deserializeUser(Room.deserializeUser()); 39 | 40 | let rootPath = path.join(__dirname, '../'); 41 | let dirPath = path.join(__dirname + '/'); 42 | 43 | const server = https.createServer({ 44 | key: fs.readFileSync(rootPath + 'server.key'), 45 | cert: fs.readFileSync(rootPath + 'server.cert') 46 | }, app); 47 | 48 | app.use('/api', apiRoutes); 49 | 50 | app.get('/', (req, res) => { 51 | res.sendFile('index.html', {root: __dirname}); 52 | }); 53 | app.get('/status', (req, res)=> { 54 | res.sendFile(dirPath + 'index.html'); 55 | }) 56 | // CONFIG PEER SERVER 57 | const options = { 58 | debug: true 59 | } 60 | const peerserver = ExpressPeerServer(server, options); 61 | 62 | peerserver.on('connection', (id) => { 63 | console.log("Connected: ", id); 64 | peerserver.emit({cmd: "connection", user: id}); 65 | }); 66 | 67 | peerserver.on('disconnect', (id) => { 68 | console.log("Disconnected: ", id); 69 | }); 70 | 71 | app.use('/peerjs', peerserver); 72 | 73 | 74 | require('./db/seed'); 75 | 76 | module.exports = server; 77 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | } 9 | 10 | .App-header { 11 | background-color: #282c34; 12 | min-height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | font-size: calc(10px + 2vmin); 18 | color: white; 19 | } 20 | 21 | .App-link { 22 | color: #61dafb; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { 27 | transform: rotate(0deg); 28 | } 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const serverAPI = axios.create({ 4 | baseURL: '/api' 5 | }); -------------------------------------------------------------------------------- /src/components/AdminView/AdminView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RoomList from '../RoomList/RoomList'; 3 | import UsersList from '../UsersList/UsersList'; 4 | import BooksList from '../BooksList/BooksList'; 5 | import NavBar from '../NavBar/NavBar'; 6 | import adminContext from '../../contexts/adminContext'; 7 | 8 | class AdminView extends React.Component{ 9 | renderDisplayContent = () => { 10 | if(!this.context.activeMenuItem){ 11 | return
Loading...
12 | } 13 | if(this.context.activeMenuItem === "Rooms"){ 14 | return 15 | } 16 | if(this.context.activeMenuItem === "Books"){ 17 | return 18 | } 19 | if(this.context.activeMenuItem === "Users"){ 20 | return 21 | } 22 | } 23 | render(){ 24 | return( 25 |
26 | 27 | {this.renderDisplayContent()} 28 |
29 | ) 30 | } 31 | } 32 | AdminView.contextType = adminContext; 33 | export default AdminView; -------------------------------------------------------------------------------- /src/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ModalTemplate from '../Modal/Modal'; 3 | import LoginForm from '../LoginForm/LoginForm'; 4 | import appContext from '../../contexts/appContext'; 5 | import UserView from '../UserView/UserView'; 6 | import AdminView from '../AdminView/AdminView'; 7 | import {AdminContextStore} from '../../contexts/adminContext'; 8 | import {UserContextStore} from '../../contexts/userContext'; 9 | 10 | class App extends Component{ 11 | renderApp = () => { 12 | if(!this.context.room){ 13 | return( 14 |
15 | } actions="" /> 16 |
17 | ) 18 | } 19 | if(this.context.room.isAdmin === false){ 20 | return ( 21 |
22 | 23 | 24 | 25 |
26 | ) 27 | } 28 | if(this.context.room.isAdmin === true){ 29 | return ( 30 |
31 | 32 | 33 | 34 |
35 | ) 36 | } 37 | } 38 | render(){ 39 | return( 40 |
41 | {this.renderApp()} 42 |
43 | ) 44 | } 45 | } 46 | 47 | App.contextType = appContext; 48 | export default App; -------------------------------------------------------------------------------- /src/components/BookCreateForm/BookCreateForm.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Form, Message ,Button, Dimmer, Loader} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | 5 | class BookCreateForm extends Component{ 6 | constructor(props){ 7 | super(props); 8 | this.parts = null; 9 | } 10 | state = { 11 | name: null, 12 | author: null, 13 | err: null, 14 | showLoader: false 15 | } 16 | validateForm = () => { 17 | if(!this.state.name || this.state.name.length < 2){ 18 | return this.setState({err: "Invalid name"}) 19 | } 20 | if(!this.state.author || this.state.author.length < 2){ 21 | return this.setState({err: "Invalid author"}) 22 | } 23 | this.setState({err: null, showLoader: true}); 24 | this.handleUpload() 25 | } 26 | handleUpload = () => { 27 | const data = new FormData(); 28 | data.set("name", this.state.name) 29 | data.set("author", this.state.author) 30 | for(let i = 0; i < this.parts.length; i++){ 31 | data.append(`file${i}`, this.parts[i], this.parts[i].name) 32 | } 33 | this.context.createNewBook(data); 34 | } 35 | setName = (event) => { 36 | this.setState({name: event.target.value}) 37 | } 38 | setAuthor = (event) => { 39 | this.setState({author: event.target.value}) 40 | } 41 | setParts = (event) => { 42 | this.parts = event.target.files 43 | } 44 | renderErrorMessage = () => { 45 | if(this.state.err === null){ 46 | return; 47 | } 48 | return ( 49 | 50 | ) 51 | } 52 | renderContent = () => { 53 | if(this.state.showLoader){ 54 | return ( 55 | 56 | Uploading Files... 57 | 58 | ) 59 | } else { 60 | return ( 61 |
62 | {this.renderErrorMessage()} 63 | 64 | 65 | {this.setName(e)}} autoFocus required/> 66 | 67 | 68 | 69 | {this.setAuthor(e)}} required/> 70 | 71 | 72 | 73 | {this.setParts(e)}} required/> 74 | 75 | 76 |
77 | ) 78 | } 79 | } 80 | render(){ 81 | return( 82 |
83 | {this.renderContent()} 84 |
85 | ) 86 | } 87 | }; 88 | 89 | BookCreateForm.contextType = adminContext; 90 | export default BookCreateForm; 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/components/BooksList/BooksList.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Card, Button, Icon} from 'semantic-ui-react'; 3 | import ConfirmTemplate from '../Confirm/Confim'; 4 | import {serverAPI} from '../../api/api'; 5 | import adminContext from '../../contexts/adminContext'; 6 | 7 | let INITIAL_STATE = {showConfirm: false, selectedBook: null} 8 | 9 | class BooksList extends Component{ 10 | state = INITIAL_STATE 11 | handleConfirm = book => { 12 | this.deleteBook(book) 13 | } 14 | handleCancel = () => { 15 | this.setState({showConfirm: false, selectedBook: null}) 16 | } 17 | deleteBook = book => { 18 | serverAPI.delete(`/books/${this.state.selectedBook._id}`) 19 | .then(() => { 20 | this.setState({showConfirm: false, selectedBook: null}); 21 | this.context.fetchBooksList(); 22 | }) 23 | .catch(err => console.log(err)); 24 | } 25 | renderAuthor = book => { 26 | if(!book.author){ 27 | return 28 | } 29 | return {book.author} 30 | } 31 | renderBooksList = () => { 32 | let books = this.context.books; 33 | if(!books || books.length < 1){ 34 | return
Loading books...
35 | } else { 36 | return books.map(book => { 37 | return ( 38 | 39 | 40 | {book.name} 41 | 42 |

Author: {this.renderAuthor(book)}

43 | {book.parts.length} parts 44 |
45 | 46 |
47 | 54 | 62 |
63 |
64 |
65 |
66 | ) 67 | }) 68 | } 69 | } 70 | renderContent = () => { 71 | if(!this.state.showConfirm && !this.state.selectedBook){ 72 | return ( 73 | 74 | {this.renderBooksList()} 75 | 76 | ) 77 | } else { 78 | let content = `Are you sure you want to delete "${this.state.selectedBook.name}" ?`; 79 | return ( 80 |
81 | 88 |
89 | ) 90 | } 91 | } 92 | componentDidMount(){ 93 | this.context.fetchBooksList() 94 | } 95 | render(){ 96 | return ( 97 |
98 | {this.renderContent()} 99 |
100 | ) 101 | } 102 | } 103 | BooksList.contextType = adminContext; 104 | export default BooksList; -------------------------------------------------------------------------------- /src/components/Confirm/Confim.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {Confirm, Button} from 'semantic-ui-react'; 3 | 4 | class ConfirmTemplate extends Component{ 5 | constructor(props){ 6 | super(props); 7 | } 8 | render(){ 9 | return( 10 |
11 | this.props.handleConfirm()} 14 | onCancel={() => this.props.handleCancel()} 15 | onClose={() => this.props.handleCancel()} 16 | header={this.props.header} 17 | content={this.props.content} 18 | /> 19 |
20 | ) 21 | } 22 | } 23 | 24 | 25 | export default ConfirmTemplate; -------------------------------------------------------------------------------- /src/components/LoginForm/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Form, Message ,Button} from 'semantic-ui-react'; 3 | import appContext from '../../contexts/appContext'; 4 | 5 | class LoginForm extends Component{ 6 | state = { 7 | username: null, 8 | password: null, 9 | err: null 10 | } 11 | validateForm = () => { 12 | if(!this.state.username || this.state.username.length < 3){ 13 | return this.setState({err: "Invalid username"}) 14 | } 15 | if(!this.state.password || this.state.password.length < 3){ 16 | return this.setState({err: "Invalid password"}) 17 | } 18 | this.setState({err: null}); 19 | return this.context.fetchRoomData({username: this.state.username, password: this.state.password}); 20 | } 21 | setUsername = (event) => { 22 | this.setState({username: event.target.value}) 23 | } 24 | setPassword = (event) => { 25 | this.setState({password: event.target.value}) 26 | } 27 | renderErrorMessage = () => { 28 | if(this.state.err === null){ 29 | return; 30 | } 31 | return ( 32 | 33 | ) 34 | } 35 | render(){ 36 | return( 37 |
38 | {this.renderErrorMessage()} 39 | 40 | 41 | {this.setUsername(e)}} autoFocus required/> 42 | 43 | 44 | 45 | {this.setPassword(e)}} required/> 46 | 47 | 48 |
49 | ) 50 | } 51 | }; 52 | 53 | LoginForm.contextType = appContext; 54 | export default LoginForm; 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Modal, Header, Button, Icon, Segment} from 'semantic-ui-react'; 3 | 4 | class ModalTemplate extends Component{ 5 | renderTriggerButton = () => { 6 | return ( 7 |
8 | 9 | 14 | 15 |
16 | ) 17 | } 18 | render(){ 19 | return( 20 | 21 |
22 | 23 | {this.props.content} 24 | 25 | 26 | ) 27 | } 28 | } 29 | 30 | export default ModalTemplate; -------------------------------------------------------------------------------- /src/components/ModalNav/ModalNav.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Modal, Header, Button, Icon, Segment} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | 5 | 6 | class ModalNavTemplate extends Component{ 7 | renderTriggerButton = () => { 8 | return ( 9 |
10 | 11 | 16 | 17 |
18 | ) 19 | } 20 | render(){ 21 | return( 22 | this.context.dispalyNavModal(false) }> 23 |
24 | 25 | {this.props.content} 26 | 27 | 28 | ) 29 | } 30 | } 31 | 32 | ModalNavTemplate.contextType = adminContext; 33 | export default ModalNavTemplate; -------------------------------------------------------------------------------- /src/components/NavBar/NavBar.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Menu, Input} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | import ModalNav from '../ModalNav/ModalNav'; 5 | import UserCreateForm from '../UserCreateForm/UserCreateForm'; 6 | import BookCreateForm from '../BookCreateForm/BookCreateForm'; 7 | 8 | class NavBar extends Component{ 9 | renderAddButton = () => { 10 | if(!this.context.activeMenuItem){ 11 | return 12 | } 13 | if(this.context.activeMenuItem === 'Books'){ 14 | return } actions="" icon="book"/> 15 | } 16 | if(this.context.activeMenuItem === 'Users'){ 17 | return } actions="" icon="add user"/> 18 | } 19 | } 20 | renderSearchBox = () => { 21 | if(!this.context.activeMenuItem){ 22 | return
23 | } 24 | if(this.context.activeMenuItem === 'Books'){ 25 | return ( 26 | 32 | ) 33 | } 34 | if(this.context.activeMenuItem === 'Users'){ 35 | return ( 36 | 42 | ) 43 | } 44 | } 45 | renderMenu = () => { 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | {this.renderAddButton()} 53 | {this.renderSearchBox()} 54 | 55 | 56 | ) 57 | } 58 | render(){ 59 | return( 60 |
61 | {this.renderMenu()} 62 |
63 | ) 64 | } 65 | } 66 | 67 | NavBar.contextType = adminContext; 68 | export default NavBar -------------------------------------------------------------------------------- /src/components/RoomControls/RoomControls.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, Segment, Icon, Header, ButtonGroup} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | const Peer = window.Peer; 5 | 6 | let ctx; 7 | let adminMicGain; 8 | let outputToUser; 9 | let startCtx; 10 | let disconnectButton; 11 | let adminPlayer; 12 | let talkBackButton; 13 | let userMicGainSlider; 14 | let timeDisplay; 15 | let userPlayerGainSlider; 16 | let startUserPlayerBtn; 17 | let pauseUserPlayerBtn; 18 | let stopUserPlayerBtn; 19 | let rewind 20 | let fforward; 21 | let oscActive; 22 | let oscGain; 23 | 24 | class RoomControls extends React.Component{ 25 | constructor(props){ 26 | super(props) 27 | this.state = { 28 | call: null 29 | } 30 | } 31 | renderRoomMixControls = () => { 32 | return( 33 | 34 |

Mic Volume

35 | 45 |

Sound Volume

46 | 56 |
57 | ) 58 | } 59 | renderOscControls = () => { 60 | return( 61 | 62 |

OSC

63 | 64 | 74 |
75 | ) 76 | } 77 | renderPlayerControls = () => { 78 | let isDisabled = this.context.onlineRooms.includes(this.context.selectedRoom.username) ? false : true; 79 | return ( 80 | 81 |

Connection

82 | 83 | 108 | 109 | 110 | ) 111 | } else { 112 | return 113 | } 114 | } 115 | updateCurrentRoomData = () => { 116 | let data = { 117 | currentUser: this.state.currentUser, 118 | currentBook: this.state.currentBook, 119 | currentPart: this.state.currentPart 120 | } 121 | serverAPI.put(`/rooms/${this.context.selectedRoom._id}`, data) 122 | .then(() => { 123 | this.context.getAllRooms(); 124 | this.props.handleModalClose(); 125 | }) 126 | .catch(err => console.log(err)); 127 | } 128 | clearCurrentRoomData = async () => { 129 | await this.setState({ 130 | currentUser: null, 131 | currentBook: null, 132 | currentPart: null 133 | }) 134 | this.updateCurrentRoomData(); 135 | } 136 | setCurrentState = () => { 137 | let selectedRoom = this.context.selectedRoom; 138 | if(!selectedRoom){ 139 | return; 140 | } else{ 141 | let currentUser = selectedRoom.currentUser ? selectedRoom.currentUser._id : null; 142 | let currentBook = selectedRoom.currentBook ? selectedRoom.currentBook._id : null; 143 | let currentPart = selectedRoom.currentPart ? selectedRoom.currentPart : null; 144 | this.setState({currentUser, currentBook, currentPart}); 145 | } 146 | } 147 | componentDidMount(){ 148 | this.context.fetchUsersList(); 149 | this.context.fetchBooksList(); 150 | this.setCurrentState(); 151 | } 152 | render(){ 153 | return( 154 | 155 |

Select User

156 | {this.renderUserSelection()} 157 |

Select Book

158 | {this.renderBookSelection()} 159 |

Select File

160 | {this.renderBookParts()} 161 |
162 | {this.renderButtons()} 163 |
164 | ) 165 | } 166 | } 167 | 168 | RoomEdit.contextType = adminContext; 169 | export default RoomEdit; 170 | -------------------------------------------------------------------------------- /src/components/RoomList/RoomList.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Button, Card, Icon, Container} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | import RoomEdit from '../RoomEdit/RoomEdit'; 5 | import RoomControls from '../RoomControls/RoomControls'; 6 | import RoomModalTemplate from '../RoomModal/RoomModal'; 7 | 8 | const INITIAL_STATE = { 9 | showModal: false 10 | } 11 | 12 | class RoomList extends Component{ 13 | state = INITIAL_STATE; 14 | handleModalClose = () => { 15 | this.setState({showModal: false}); 16 | this.context.setSelectedRoom(null); 17 | } 18 | showModal = (room, mode) => { 19 | this.setState({showModal: true}) 20 | if(mode === "edit"){ 21 | this.context.editSelectedRoom(room) 22 | } 23 | if(mode === "view"){ 24 | this.context.setSelectedRoom(room) 25 | } 26 | } 27 | renderRoomControlsOrEdit = () => { 28 | if(!this.context.selectedRoom){ 29 | return
30 | } else if(this.context.selectedRoom && this.context.editRoom && this.state.showModal){ 31 | let header = `Edit ${this.context.selectedRoom.username}`; 32 | return ( 33 | }/> 34 | ) 35 | } else if(this.context.selectedRoom && this.state.showModal){ 36 | let header = `${this.context.selectedRoom.username} Controls`; 37 | return ( 38 | }/> 39 | ) 40 | } else { 41 | return
42 | } 43 | } 44 | renderCardExtraContent = (room) => { 45 | return( 46 | 47 |
48 | 49 | 50 |
51 |
52 | ) 53 | } 54 | renderCardMeta = room => { 55 | if(!room.currentUser){ 56 | return 57 | } else { 58 | return {room.currentUser.firstName} {room.currentUser.lastName} 59 | } 60 | } 61 | renderCardDescription = room => { 62 | if(!room.currentBook){ 63 | return 64 | } else { 65 | return {room.currentBook.name}
{ room.currentPart }
66 | } 67 | 68 | } 69 | renderCardContent = (room, color) => { 70 | return( 71 | 72 | {this.context.setSelectedRoom(room)}}>{room.username} 73 | {this.renderCardMeta(room)} 74 | {this.renderCardDescription(room)} 75 | 76 | ) 77 | } 78 | setStatusIconColor = room => { 79 | if(this.context.currentCall && this.context.currentCall === room.username){ 80 | return 'orange'; 81 | } else { 82 | return this.context.onlineRooms.includes(room.username) ? 'green' : 'grey'; 83 | } 84 | } 85 | renderRoomlist = () => { 86 | if(!this.props.rooms || this.props.rooms.length < 1){ 87 | return
Loading rooms...
88 | } 89 | return ( 90 | 91 | {this.props.rooms.map((room, index) => { 92 | let color = this.setStatusIconColor(room) 93 | if(room.isAdmin === true){ 94 | return 95 | } else if(this.context.selectedRoom && room.username === this.context.selectedRoom.username) { 96 | return ( 97 | 98 | {this.renderCardContent(room, color)} 99 | {this.renderCardExtraContent(room)} 100 | {this.renderRoomControlsOrEdit()} 101 | 102 | ) 103 | } else { 104 | return ( 105 | 106 | {this.renderCardContent(room, color)} 107 | {this.renderCardExtraContent(room)} 108 | 109 | ) 110 | } 111 | })} 112 | 113 | ) 114 | } 115 | render(){ 116 | // console.log(this.context); 117 | return( 118 | 119 |

Room List

120 | {this.renderRoomlist()} 121 |
122 | ) 123 | } 124 | } 125 | 126 | RoomList.contextType = adminContext; 127 | export default RoomList -------------------------------------------------------------------------------- /src/components/RoomModal/RoomModal.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Modal, Header} from 'semantic-ui-react'; 3 | 4 | class RoomModalTemplate extends Component{ 5 | render(){ 6 | return( 7 | this.props.handleModalClose() }> 8 |
9 | 10 | {this.props.content} 11 | 12 | 13 | ) 14 | } 15 | } 16 | 17 | export default RoomModalTemplate; -------------------------------------------------------------------------------- /src/components/UserCreateForm/UserCreateForm.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Form, Message ,Button} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | 5 | class UserCreateForm extends Component{ 6 | state = { 7 | firstName: null, 8 | lastName: null, 9 | email: null, 10 | err: null 11 | } 12 | validateForm = () => { 13 | if(!this.state.firstName || this.state.firstName.length < 2){ 14 | return this.setState({err: "Invalid firstName"}) 15 | } 16 | if(!this.state.lastName || this.state.lastName.length < 2){ 17 | return this.setState({err: "Invalid lastName"}) 18 | } 19 | this.setState({err: null}); 20 | this.context.createNewUser({firstName: this.state.firstName, lastName: this.state.lastName, email: this.state.email}); 21 | } 22 | setFirstName = (event) => { 23 | this.setState({firstName: event.target.value}) 24 | } 25 | setLastName = (event) => { 26 | this.setState({lastName: event.target.value}) 27 | } 28 | setEmail = (event) => { 29 | this.setState({email: event.target.value}) 30 | } 31 | renderErrorMessage = () => { 32 | if(this.state.err === null){ 33 | return; 34 | } 35 | return ( 36 | 37 | ) 38 | } 39 | render(){ 40 | return( 41 |
42 | {this.renderErrorMessage()} 43 | 44 | 45 | {this.setFirstName(e)}} autoFocus required/> 46 | 47 | 48 | 49 | {this.setLastName(e)}} required/> 50 | 51 | 52 | 53 | {this.setEmail(e)}} required/> 54 | 55 | 56 |
57 | ) 58 | } 59 | }; 60 | 61 | UserCreateForm.contextType = adminContext; 62 | export default UserCreateForm; 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/components/UserView/UserView.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Button, Icon} from 'semantic-ui-react'; 3 | import userContext from '../../contexts/userContext'; 4 | 5 | 6 | 7 | class UserView extends Component{ 8 | renderView = () => { 9 | let room = this.props.room; 10 | if(!room || !room.currentUser || !room.currentBook || !room.currentPart ){ 11 | return ( 12 | 44 | 45 | 46 | 47 | ) 48 | }) 49 | } 50 | } 51 | renderContent = () => { 52 | if(!this.state.showConfirm && !this.state.selectedUser){ 53 | return( 54 | 55 | {this.renderUsersList()} 56 | 57 | ) 58 | } else { 59 | let content = `Are you sure you want to delete "${this.state.selectedUser.firstName} ${this.state.selectedUser.lastName}" ?`; 60 | return ( 61 |
62 | 69 |
70 | ) 71 | } 72 | } 73 | componentDidMount(){ 74 | this.context.fetchUsersList() 75 | } 76 | render(){ 77 | return ( 78 |
79 | {this.renderContent()} 80 |
81 | ) 82 | } 83 | } 84 | 85 | UsersList.contextType = adminContext; 86 | export default UsersList; -------------------------------------------------------------------------------- /src/contexts/adminContext.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {serverAPI} from '../api/api' 3 | import PeerClient from 'peerjs'; 4 | const Context = React.createContext(); 5 | const Peer = window.Peer; 6 | 7 | const peerConfig = { 8 | host: window.location.hostname, 9 | port: '8080', 10 | path: '/peerjs', 11 | sdpSemantics: 'unified-plan', 12 | debug: 0, 13 | } 14 | const INITIAL_STATE = { 15 | activeMenuItem: "Rooms", 16 | peer: null, 17 | isPeerInitialized: false, 18 | isNavModalOpen: false, 19 | rooms: [], 20 | users: [], 21 | books: [], 22 | onlineRooms: [], 23 | selectedRoom: null, 24 | editRoom: false, 25 | currentConnection: null, 26 | currentCall: null, 27 | connData: {}, 28 | userMicGainSlider: 50, 29 | userPlayerGainSlider: 70, 30 | } 31 | 32 | export class AdminContextStore extends Component{ 33 | constructor(props){ 34 | super(props) 35 | this.state = INITIAL_STATE; 36 | } 37 | handleMenuItemClick = (e, {name}) => { 38 | this.setState({activeMenuItem: name, isNavModalOpen: false}) 39 | } 40 | dispalyNavModal = isNavModalOpen => { 41 | this.setState({isNavModalOpen }); 42 | } 43 | createNewUser = user => { 44 | serverAPI.post('/users', user) 45 | .then(() => this.fetchUsersList()) 46 | .catch(err => console.log(err)); 47 | } 48 | fetchUsersList = () => { 49 | serverAPI.get('/users') 50 | .then(res => this.setState({ users: res.data, isNavModalOpen: false})) 51 | .catch(err => { console.log(err) }); 52 | } 53 | fetchBooksList = () => { 54 | serverAPI.get('/books') 55 | .then(res => this.setState({books: res.data, isNavModalOpen: false})) 56 | .catch(err => { console.log(err) }); 57 | } 58 | createNewBook = book => { 59 | serverAPI.post('/books', book) 60 | .then(() => { this.fetchBooksList() }) 61 | .catch(err => console.log(err)); 62 | } 63 | getAllRooms = async () => { 64 | serverAPI.get('/rooms') 65 | .then((res) => { 66 | this.setState({rooms: res.data}); 67 | }) 68 | .catch((err) => {console.log(err)}); 69 | } 70 | setOnlineRooms = onlineRooms => { 71 | this.setState({onlineRooms}); 72 | } 73 | setPeerInitialized = isPeerInitialized => { 74 | this.setState({isPeerInitialized}) 75 | } 76 | setSelectedRoom = selectedRoom =>{ 77 | this.setState({selectedRoom, editRoom: false, currentCall: null, currentConnection: null, connData: null}); 78 | } 79 | setCurrentConnection = currentConnection => { 80 | console.log("SET NEW CONNECTION"); 81 | this.setState({currentConnection}); 82 | } 83 | editSelectedRoom = selectedRoom => { 84 | this.setState({selectedRoom, editRoom: true, currentCall: null}) 85 | } 86 | setPeerConnection = async () => { 87 | await this.setState({peer: new Peer('admin', peerConfig ), isPeerInitialized: true }); 88 | } 89 | setCurrentCommand = cmd => { 90 | this.setState({cmd}) 91 | } 92 | setCurrentCall = call => { 93 | if(!this.state.currentCall){ 94 | this.setState({currentCall: call}); 95 | } 96 | } 97 | hangCurrentCall = () => { 98 | if(this.state.currentCall){ 99 | console.log("HANGING UP CURRENT CALL"); 100 | this.setState({currentCall: null}); 101 | } 102 | } 103 | componentDidMount(){ 104 | this.setPeerConnection().then(()=>{ 105 | this.state.peer.on('connection', (conn)=>{ 106 | let newOnlineRoomsList = [...this.state.onlineRooms, conn.peer]; 107 | this.setOnlineRooms(newOnlineRoomsList); 108 | conn.on('open', () => { 109 | conn.send({cmd: "admin connect"}); 110 | }) 111 | conn.on('close', ()=>{ 112 | let newOnlineRoomsList = this.state.onlineRooms.filter(room => room !== conn.peer); 113 | this.setOnlineRooms(newOnlineRoomsList); 114 | }); 115 | conn.on('data', (data) => { 116 | if(data.cmd ==="update"){ 117 | this.setState({connData: data, userMicGainSlider: data.micGain * 100, userPlayerGainSlider: data.playerGain * 100}); 118 | } 119 | if(data.cmd === "user answered"){ 120 | this.setCurrentConnection(conn) 121 | } 122 | }); 123 | }) 124 | this.getAllRooms() 125 | }) 126 | .catch(err => console.log(err)) 127 | } 128 | componentWillUnmount(){ 129 | this.state.peer.destroy(); 130 | } 131 | render(){ 132 | return( 133 | 150 | {this.props.children} 151 | 152 | ) 153 | } 154 | } 155 | 156 | export default Context; -------------------------------------------------------------------------------- /src/contexts/appContext.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {serverAPI} from '../api/api'; 3 | const Context = React.createContext(); 4 | 5 | export class AppContextStore extends Component{ 6 | state = { 7 | room: null 8 | } 9 | setRoom = room => { 10 | this.setState({room}); 11 | } 12 | fetchRoomData = loginData => { 13 | serverAPI.post('/login', loginData) 14 | .then((res) => { 15 | if(res.data.room){ 16 | this.setRoom(res.data.room) 17 | } 18 | }); 19 | } 20 | render(){ 21 | return( 22 | 26 | {this.props.children} 27 | 28 | ) 29 | } 30 | } 31 | 32 | 33 | export default Context; -------------------------------------------------------------------------------- /src/contexts/userContext.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import pitchFinder from 'pitchfinder'; 3 | import appContext from './appContext'; 4 | import PeerClient from 'peerjs'; 5 | 6 | const Context = React.createContext(); 7 | const Peer = window.Peer; 8 | 9 | const peerConfig = { 10 | host: window.location.hostname, 11 | port: '8080', 12 | path: '/peerjs', 13 | sdpSemantics: 'unified-plan', 14 | debug: 0, 15 | } 16 | 17 | const INITIAL_STATE = { 18 | isOnline: false, 19 | isPeerInitialized: false, 20 | isAdminConnected: false, 21 | isPlaying: false, 22 | micGainValue: 0.5, 23 | playerGainValue: 0.8, 24 | isOscActive: true, 25 | isOscPlaying: false, 26 | oscGainValue: 0.001 27 | } 28 | 29 | export class UserContextStore extends Component{ 30 | state = INITIAL_STATE 31 | constructor(props){ 32 | super(props) 33 | this.ctx = null; 34 | this.peer = null; 35 | this.micToAdminGain = null; 36 | this.playerToAdminGain = null; 37 | this.playerGain = null; 38 | this.micGain = null; 39 | this.userPlayer = null; 40 | this.analyser = null; 41 | this.sampleRate = null; 42 | this.pitchArr = []; 43 | this.detectPitch = pitchFinder.YIN(); 44 | this.updateAdminTimer = null; 45 | this.adminMicPlayer = null; 46 | this.micStream = null; 47 | this.playerStream = null; 48 | this.micStreamToOutput = null; 49 | this.clonedMicStream = null; 50 | this.clonedPlayerStream = null; 51 | } 52 | setPeerConnection = async () => { 53 | this.peer = new Peer(this.context.room.username, peerConfig ); 54 | await this.setState({isPeerInitialized: true }); 55 | } 56 | initUserPlayback = async userPlayerElement => { 57 | this.ctx = await new(window.AudioContext || window.webkitAudioContext)(); 58 | this.userPlayer = userPlayerElement; 59 | this.analyser = this.ctx.createAnalyser(); 60 | this.sampleRate = this.ctx.sampleRate; 61 | // Capture Stream from audio player 62 | if(this.isChromeBrowser()){ 63 | this.playerStream = await this.userPlayer.captureStream(); 64 | } else { 65 | this.playerStream = await this.userPlayer.mozCaptureStream(); 66 | } 67 | this.playerGain = this.ctx.createGain() 68 | this.playerGain.gain.value = this.state.playerGainValue; 69 | let playerStreamToOutput = this.ctx.createMediaStreamSource(this.playerStream); 70 | let filter = this.ctx.createBiquadFilter(); 71 | filter.type = "highpass"; 72 | filter.frequency.value = 250; 73 | playerStreamToOutput.connect(filter); 74 | filter.connect(this.analyser); 75 | this.analyser.connect(this.playerGain); 76 | this.playerGain.connect(this.ctx.destination); 77 | this.getUserMicStream() 78 | } 79 | isChromeBrowser = () => { 80 | if(navigator.userAgent.indexOf("Chrome") >= 0){ 81 | console.log("Chrome") 82 | return true 83 | } else if(navigator.userAgent.indexOf("Firefox") >= 0){ 84 | console.log("FireFox") 85 | return false 86 | } 87 | } 88 | getUserMicStream = async () => { 89 | let constraints = { video: false, audio: true }; 90 | navigator.mediaDevices.getUserMedia(constraints) 91 | .then(stream => { 92 | this.micGain = this.ctx.createGain(); 93 | this.micStream = stream; 94 | this.micGain.gain.value = this.state.micGainValue; 95 | this.micStreamToOutput = this.ctx.createMediaStreamSource(this.micStream) 96 | this.micStreamToOutput.connect(this.micGain); 97 | this.micGain.connect(this.ctx.destination); 98 | console.log("Finished Mic Setup"); 99 | this.setupConnection(); 100 | }) 101 | .catch((err) => console.error(err)); 102 | } 103 | setupConnection = async () =>{ 104 | let conn = await this.peer.connect('admin', {serialization: "json"}); 105 | this.setState({isOnline: true}) 106 | conn.on('open', () => { 107 | conn.send({cmd: "user online"}); 108 | }); 109 | conn.on('admin disconnect', () => { 110 | console.log("Admin disconnected"); 111 | }) 112 | conn.on('close', () => { 113 | this.setState({isOnline: false}) 114 | console.log("Admin Disconnected") 115 | clearInterval(this.updateAdminTimer); 116 | this.updateAdminTimer = null; 117 | }); 118 | conn.on('data', (data) => { 119 | if(this.state.isAdminConnected){ 120 | if(data.cmd === "admin connect"){ 121 | console.log("Admin Connected"); 122 | } 123 | if(data.cmd === "mic gain"){ 124 | this.setState({micGainValue: data.value}) 125 | this.micGain.gain.setValueAtTime(data.value, this.ctx.currentTime); 126 | this.micToAdminGain.gain.setValueAtTime(data.value, this.ctx.currentTime); 127 | } 128 | if(data.cmd === "player gain"){ 129 | this.setState({playerGainValue: data.value}) 130 | this.playerGain.gain.setValueAtTime(data.value, this.ctx.currentTime); 131 | this.playerToAdminGain.gain.setValueAtTime(data.value, this.ctx.currentTime); 132 | } 133 | if(data.cmd === "osc state"){ 134 | if(data.value === false){ 135 | this.setState({isOscActive: false}); 136 | } else { 137 | this.setState({isOscActive: true}); 138 | }; 139 | } 140 | if(data.cmd === "osc gain"){ 141 | this.setState({oscGainValue: data.value}) 142 | } 143 | if(data.cmd === "player start"){ 144 | this.userPlayer.play(); 145 | this.setState({isPlaying: true}) 146 | this.pitchDetector(); 147 | } 148 | if(data.cmd === "player pause"){ 149 | this.setState({isPlaying: false}); 150 | this.userPlayer.pause(); 151 | } 152 | if(data.cmd === "player stop"){ 153 | this.setState({isPlaying: false}) 154 | this.userPlayer.currentTime = 0; 155 | this.userPlayer.pause(); 156 | } 157 | if(data.cmd === "rewind"){ 158 | let newTransportPosition = this.userPlayer.currentTime - 30 > 0 ? this.userPlayer.currentTime - 30 : 0; 159 | this.userPlayer.currentTime = newTransportPosition; 160 | } 161 | if(data.cmd === "fforward"){ 162 | let maxDuration = this.userPlayer.duration; 163 | let newTransportPosition = this.userPlayer.currentTime + 30 < maxDuration ? this.userPlayer.currentTime + 30 : maxDuration; 164 | this.userPlayer.currentTime = newTransportPosition; 165 | } 166 | } 167 | this.peer.on('call', (call) => { 168 | console.log("Got call from admin"); 169 | 170 | this.createStreamToAdmin(call, conn) 171 | call.on('close', () => { 172 | console.log("CALL ENDED"); 173 | clearInterval(this.updateAdminTimer); 174 | if(this.adminMicPlayer){ 175 | this.adminMicPlayer.pause(); 176 | this.adminMicPlayer.currentTime = 0; 177 | } 178 | this.setState({isAdminConnected: false, updateAdminTimer: null, adminMicPlayer: null}); 179 | }) 180 | call.on('error', () => { 181 | console.log("CALL ENDED ON ERROR"); 182 | this.setState({isAdminConnected: false}) 183 | }) 184 | this.setState({isAdminConnected: true}) 185 | conn.send({cmd: 'user answered'}) 186 | }); 187 | }); 188 | 189 | } 190 | createStreamToAdmin = async (call, conn) => { 191 | this.clonedMicStream = this.micStream.clone(); 192 | this.clonedPlayerStream = this.playerStream.clone(); 193 | let streamToAdmin = this.ctx.createMediaStreamDestination(); 194 | let micStreamToAdmin = this.ctx.createMediaStreamSource(this.clonedMicStream); 195 | let playerStreamToAdmin = this.ctx.createMediaStreamSource(this.clonedPlayerStream); 196 | // Stream volume mix controls 197 | this.micToAdminGain = this.ctx.createGain() 198 | this.playerToAdminGain = this.ctx.createGain() 199 | // Init mix controls gain 200 | this.micToAdminGain.gain.setValueAtTime(this.state.micGainValue, this.ctx.currentTime); 201 | this.playerToAdminGain.gain.setValueAtTime(this.state.playerGainValue, this.ctx.currentTime); 202 | micStreamToAdmin.connect(this.micToAdminGain); 203 | playerStreamToAdmin.connect(this.playerToAdminGain) 204 | this.micToAdminGain.connect(streamToAdmin); 205 | this.playerToAdminGain.connect(streamToAdmin); 206 | call.answer(streamToAdmin.stream) 207 | call.on('stream', (adminStream) => { 208 | this.adminMicPlayer = document.getElementById('adminMicPlayer'); 209 | this.adminMicPlayer.srcObject= adminStream; 210 | this.adminMicPlayer.play() 211 | }); 212 | const updateAdminUi = () => { 213 | if(this.state.isAdminConnected){ 214 | conn.send({ 215 | cmd: "update" , 216 | micGain: this.state.micGainValue, 217 | playerGain: this.state.playerGainValue, 218 | isPlaying: this.state.isPlaying, 219 | playerTime: this.userPlayer.currentTime, 220 | isAdminConnected: this.state.isAdminConnected, 221 | isOnline: this.state.isOnline 222 | }); 223 | } 224 | } 225 | this.updateAdminTimer = setInterval(() => { 226 | updateAdminUi() 227 | }, 1000); 228 | } 229 | pitchDetector = () => { 230 | if(!this.state.isPlaying){ 231 | return; 232 | } 233 | // Populate the dataArray from the analyser method 234 | let dataArray = new Uint8Array(this.analyser.fftSize) 235 | this.analyser.getByteTimeDomainData(dataArray); 236 | // Detect pitch and push to array; 237 | let pitch = this.detectPitch(dataArray, { sampleRate: 48000}); 238 | this.pitchArr = [...this.pitchArr, pitch] 239 | if(this.pitchArr.length > 2){ 240 | let newPitchArr = this.pitchArr; 241 | newPitchArr.shift() 242 | this.pitchArr = newPitchArr; 243 | } 244 | // Pervent overloading the oscillator 245 | if(this.state.isOscActive && !this.state.isOscPlaying && this.pitchArr[1] && this.pitchArr[1] !== this.pitchArr[0]){ 246 | this.playOsc(this.pitchArr[1]) 247 | } 248 | requestAnimationFrame(this.pitchDetector) 249 | } 250 | playOsc = pitch => { 251 | let osc = this.ctx.createOscillator(); 252 | let oscGain = this.ctx.createGain(); 253 | osc.type = 'sine'; 254 | osc.frequency.setValueAtTime(pitch*100, this.ctx.currentTime); 255 | oscGain.gain.value = this.state.oscGainValue; 256 | osc.connect(oscGain); 257 | oscGain.connect(this.ctx.destination) 258 | this.setState({isOscPlaying: true}) 259 | setTimeout(() => { this.setState({isOscPlaying: false})}, 1000); 260 | osc.start(); 261 | oscGain.gain.setValueAtTime(0, this.ctx.currentTime + 0.8) 262 | setTimeout(() => { 263 | osc.stop(); 264 | }, 1000) 265 | } 266 | componentDidMount() { 267 | this.setPeerConnection() 268 | .then(() => { 269 | console.log("Peer initialized") 270 | }) 271 | .catch(err => {console.log(err)}) 272 | } 273 | componentWillUnmount(){ 274 | if(this.ctx){ 275 | this.ctx.close(); 276 | } 277 | if(this.peer){ 278 | this.peer.close(); 279 | } 280 | } 281 | render(){ 282 | return( 283 | 287 | {this.props.children} 288 | 289 | ) 290 | } 291 | } 292 | 293 | UserContextStore.contextType = appContext; 294 | export default Context; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'semantic-ui-css/semantic.min.css'; 4 | import App from './components/App/App'; 5 | import {AppContextStore} from './contexts/appContext'; 6 | 7 | ReactDOM.hydrate( 8 | 9 | 10 | , 11 | document.getElementById('root')); 12 | 13 | // If you want your app to work offline and load faster, you can change 14 | // unregister() to register() below. Note this comes with some pitfalls. 15 | // Learn more about service workers: http://bit.ly/CRA-PWA 16 | // serviceWorker.unregister(); 17 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | --------------------------------------------------------------------------------