├── .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 | You need to enable JavaScript to run this app.
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 |
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 |
64 | Book
65 | {this.setName(e)}} autoFocus required/>
66 |
67 |
68 | Author
69 | {this.setAuthor(e)}} required/>
70 |
71 |
72 | Upload files
73 | {this.setParts(e)}} required/>
74 |
75 | {this.validateForm()}}>Submit
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 |
51 |
52 | Edit
53 |
54 | {
57 | this.setState({showConfirm: true, selectedBook: book})
58 | }}>
59 |
60 | Delete
61 |
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 |
40 | Room Name
41 | {this.setUsername(e)}} autoFocus required/>
42 |
43 |
44 | password
45 | {this.setPassword(e)}} required/>
46 |
47 | {this.validateForm()}}>Login
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 |
10 |
11 |
12 |
13 |
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 | {this.context.dispalyNavModal(true)}}>
12 |
13 |
14 |
15 |
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 |
84 |
85 |
86 | Talkback
87 |
88 |
89 |
90 | Controls
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | )
100 | }
101 | initUiElements = () => {
102 | startCtx = document.getElementById('startctx');
103 | disconnectButton = document.getElementById('disconnect');
104 | adminPlayer = document.querySelector('audio');
105 | talkBackButton = document.getElementById('talkback');
106 | userMicGainSlider = document.querySelector('#micGain');
107 | userPlayerGainSlider = document.querySelector('#playerGain');
108 | startUserPlayerBtn = document.getElementById('startUserPlayer');
109 | pauseUserPlayerBtn = document.getElementById('pauseUserPlayer');
110 | stopUserPlayerBtn = document.getElementById('stopUserPlayer');
111 | timeDisplay = document.getElementById('timeDisplay');
112 | rewind = document.getElementById('rewind');
113 | fforward = document.getElementById('fforward');
114 | oscActive = document.getElementById('oscActive');
115 | oscGain = document.getElementById('oscGain');
116 | this.disableUiElements();
117 | }
118 | disableUiElements = () => {
119 | startCtx.removeAttribute('disabled');
120 | disconnectButton.setAttribute('disabled', true);
121 | talkBackButton.setAttribute('disabled', true);
122 | startUserPlayerBtn.setAttribute('disabled', true);
123 | pauseUserPlayerBtn.setAttribute('disabled', true);
124 | stopUserPlayerBtn.setAttribute('disabled', true);
125 | rewind.setAttribute('disabled', true);
126 | fforward.setAttribute('disabled', true);
127 | userMicGainSlider.setAttribute('disabled', true);
128 | userPlayerGainSlider.setAttribute('disabled', true);
129 | }
130 | enableUiElements = () => {
131 | startCtx.setAttribute('disabled', true);
132 | startUserPlayerBtn.removeAttribute('disabled');
133 | disconnectButton.removeAttribute('disabled');
134 | talkBackButton.removeAttribute('disabled');
135 | pauseUserPlayerBtn.removeAttribute('disabled');
136 | stopUserPlayerBtn.removeAttribute('disabled');
137 | rewind.removeAttribute('disabled');
138 | fforward.removeAttribute('disabled');
139 | userMicGainSlider.removeAttribute('disabled');
140 | userPlayerGainSlider.removeAttribute('disabled');
141 | }
142 | initAudioContext = () => {
143 | startCtx.addEventListener('click', () => {
144 | this.setupAdminMic()
145 | .then(() => {
146 | this.callToUser();
147 | })
148 | });
149 | }
150 | setupControlsListeners = () => {
151 | oscActive.addEventListener('change', (e) => {
152 | this.context.currentConnection.send({cmd: "osc state", value: e.target.checked});
153 | })
154 | oscGain.addEventListener('change', (e) => {
155 | this.context.currentConnection.send({cmd: "osc gain", value: e.target.value / 10000 });
156 | })
157 | userMicGainSlider.addEventListener('change', (e) => {
158 | this.context.currentConnection.send({cmd: "mic gain", value: e.target.value / 100 });
159 | });
160 | userPlayerGainSlider.addEventListener('change', (e) => {
161 | this.context.currentConnection.send({cmd: "player gain", value: e.target.value / 100 });
162 | });
163 | talkBackButton.addEventListener('mousedown', () => {
164 | talkBackButton.style.background = "orange"
165 | adminMicGain.gain.value = 0.7;
166 | this.context.currentConnection.send("Talkback open")
167 | });
168 | talkBackButton.addEventListener('mouseup', () => {
169 | talkBackButton.style.background = "#2185d0"
170 | adminMicGain.gain.value = 0;
171 | this.context.currentConnection.send("Talkback closed")
172 | });
173 | startUserPlayerBtn.addEventListener('click', () => {
174 | this.context.currentConnection.send({cmd: "player start"})
175 | startUserPlayerBtn.setAttribute('disabled', true);
176 | stopUserPlayerBtn.removeAttribute('disabled');
177 | pauseUserPlayerBtn.removeAttribute('disabled');
178 | rewind.removeAttribute('disabled');
179 | fforward.removeAttribute('disabled');
180 | });
181 | pauseUserPlayerBtn.addEventListener('click', () => {
182 | this.context.currentConnection.send({cmd: "player pause"});
183 | pauseUserPlayerBtn.setAttribute('disabled', true);
184 | startUserPlayerBtn.removeAttribute('disabled');
185 | });
186 | stopUserPlayerBtn.addEventListener('click', () => {
187 | this.context.currentConnection.send({cmd: "player stop"});
188 | stopUserPlayerBtn.setAttribute('disabled', true);
189 | pauseUserPlayerBtn.setAttribute('disabled', true);
190 | startUserPlayerBtn.removeAttribute('disabled');
191 | });
192 | rewind.addEventListener('click', () => {
193 | this.context.currentConnection.send({cmd: "rewind"});
194 | });
195 | fforward.addEventListener('click', () => {
196 | this.context.currentConnection.send({cmd: "fforward"});
197 | })
198 | }
199 | setupAdminMic = async () => {
200 | ctx = await new(window.AudioContext || window.webkitAudioContext)();
201 | await navigator.mediaDevices.getUserMedia({audio: true})
202 | .then((micStream) => {
203 | outputToUser = ctx.createMediaStreamDestination();
204 | let micSource = ctx.createMediaStreamSource(micStream);
205 | adminMicGain = ctx.createGain();
206 | adminMicGain.gain.value = 0;
207 | micSource.connect(adminMicGain);
208 | adminMicGain.connect(outputToUser);
209 | })
210 | .catch(err => console.error(err))
211 | }
212 | callToUser = async () => {
213 | this.setState({call: this.context.peer.call(this.context.selectedRoom.username, outputToUser.stream)});
214 | disconnectButton.addEventListener('click', () => {
215 | this.endCurrentCall();
216 | });
217 | this.context.setCurrentCall(this.state.call.peer)
218 | this.state.call.on('stream', stream => {
219 | console.log("Got Stream...")
220 | this.enableUiElements();
221 | adminPlayer.srcObject = stream;
222 | adminPlayer.play();
223 | })
224 | this.state.call.on('error', (err) => {
225 | console.log(err);
226 | });
227 | }
228 | componentDidMount(){
229 | this.initUiElements();
230 | this.initAudioContext();
231 | this.setupControlsListeners();
232 | }
233 | renderUserPlayerTime = () => {
234 | if(this.context.connData && this.context.connData.playerTime){
235 | let min = Math.floor(this.context.connData.playerTime / 60)
236 | let sec = Math.round(this.context.connData.playerTime - min * 60);
237 | let timeString = `${min}:${sec}`;
238 | return (
239 |
240 | Current Player Time: {timeString}
241 |
242 | )
243 | } else {
244 | return (
245 |
246 |
247 |
248 | )
249 | }
250 | }
251 | componentWillUnmount(){
252 | this.endCurrentCall();
253 | }
254 | endCurrentCall = () => {
255 | if(this.state.call){
256 | console.log("Call Ended...");
257 | this.state.call.close();
258 | if(ctx){
259 | ctx.close().then(() => {
260 | ctx = null;
261 | })
262 | }
263 | this.disableUiElements();
264 | this.setState({call: null});
265 | this.context.setCurrentCall(null);
266 | }
267 | }
268 | render(){
269 | return(
270 |
271 |
272 | {this.renderUserPlayerTime()}
273 | {this.renderPlayerControls()}
274 | {this.renderRoomMixControls()}
275 | {this.renderOscControls()}
276 |
277 | )
278 | }
279 | }
280 |
281 | RoomControls.contextType = adminContext;
282 | export default RoomControls;
--------------------------------------------------------------------------------
/src/components/RoomEdit/RoomEdit.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Button, Segment, List, Icon} from 'semantic-ui-react';
3 | import {serverAPI} from '../../api/api';
4 | import adminContext from '../../contexts/adminContext';
5 |
6 | let INITIAL_STATE = {
7 | allUsers: null,
8 | allBooks: null,
9 | currentUser: null,
10 | currentBook: null,
11 | currentPart: null
12 | }
13 |
14 | class RoomEdit extends Component{
15 | state = INITIAL_STATE;
16 | renderCurrentRoomData = () => {
17 | let listArr = []
18 | for(let key in this.context.selectedRoom){
19 | listArr.push(key);
20 | };
21 | return listArr.map((key, index) => {
22 | return {key}: {this.context.selectedRoom[key]}
23 | });
24 | }
25 | renderUserSelection = () => {
26 | if(!this.context.users){
27 | return Loading Users...
28 | } else {
29 | let currentUserId = "";
30 | let optionContent = "";
31 | if(this.context.selectedRoom && this.context.selectedRoom.currentUser){
32 | let currentUser = this.context.selectedRoom.currentUser;
33 | currentUserId = currentUser._id;
34 | optionContent = `${currentUser.firstName} ${currentUser.lastName}`;
35 | }
36 | return (
37 | {
38 | this.setState({ currentUser: e.target.selectedOptions[0].id})}}
39 | >
40 | {optionContent}
41 | {this.renderUserList()}
42 |
43 | )
44 | }
45 | }
46 | renderUserList = () => {
47 | return this.context.users.map((user) => {
48 | return {user.firstName} {user.lastName}
49 | })
50 | }
51 | renderBookSelection = () => {
52 | let books = this.context.books
53 | if(!books || books.length < 1 ){
54 | return Loading Books Data...
55 | } else {
56 | let currentBookId = "";
57 | let optionContent = "";
58 | if(this.context.selectedRoom && this.context.selectedRoom.currentBook){
59 | let currentBook = this.context.selectedRoom.currentBook;
60 | currentBookId = currentBook._id;
61 | optionContent = `${currentBook.name}`;
62 | }
63 | return (
64 | {
65 | this.setState({currentBook: e.target.selectedOptions[0].id})}}>
66 | {optionContent}
67 | {this.renderBookList()}
68 |
69 | )
70 | }
71 | }
72 | renderBookList = () => {
73 | return this.context.books.map((book) => {
74 | return {book.name}
75 | })
76 | }
77 | renderBookParts = () => {
78 | if(!this.state.currentBook){
79 | return;
80 | } else {
81 | let currentPart = ""
82 | if(this.context.selectedRoom && this.context.selectedRoom.currentPart){
83 | currentPart = this.context.selectedRoom.currentPart;
84 | }
85 | return(
86 | {
87 | this.setState({currentPart: e.target.selectedOptions[0].id})}}>
88 | {currentPart}
89 | {this.renderPartsList()}
90 |
91 | )
92 | }
93 | }
94 | renderPartsList = () => {
95 | let books = this.context.books;
96 | if(books && books.length > 0){
97 | let selectedBook = books.filter(book => book._id === this.state.currentBook);
98 | return selectedBook[0].parts.map((part, index) => {
99 | return {part}
100 | })
101 | }
102 | }
103 | renderButtons = () =>{
104 | if(this.state.currentUser && this.state.currentBook && this.state.currentPart){
105 | return(
106 |
107 | this.clearCurrentRoomData()}>Clear
108 | this.updateCurrentRoomData()}>Update
109 |
110 | )
111 | } else {
112 | return this.clearCurrentRoomData()}>Clear
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 | {this.showModal(room, "view")}}>View
49 | {this.showModal(room, "edit")}}>Edit
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 |
44 | First Name
45 | {this.setFirstName(e)}} autoFocus required/>
46 |
47 |
48 | Last Name
49 | {this.setLastName(e)}} required/>
50 |
51 |
52 | E-Mail
53 | {this.setEmail(e)}} required/>
54 |
55 | {this.validateForm()}}>Submit
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 | {window.location.reload()}}
18 | />
19 | )
20 | } else {
21 | let isDisabled = this.context.isOnline ? true : false;
22 | let content = isDisabled ? : 'Ready'
23 | return(
24 |
25 |
{this.context.initUserPlayback(document.getElementById('userPlayer'))}}
31 | disabled={isDisabled}
32 | />
33 |
37 |
38 | )
39 | }
40 | }
41 | render(){
42 | return(
43 |
44 |
{this.props.room.username}
45 |
46 | {this.renderView()}
47 |
48 |
49 | )
50 | }
51 | }
52 |
53 | UserView.contextType = userContext;
54 | export default UserView;
55 |
56 |
--------------------------------------------------------------------------------
/src/components/UsersList/UsersList.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Card, Icon, Button} from 'semantic-ui-react';
3 | import ConfirmTemplate from '../Confirm/Confim';
4 | import {serverAPI} from '../../api/api';
5 | import adminContext from '../../contexts/adminContext';
6 |
7 | const INITIAL_STATE = { showConfirm: false, selectedUser: null}
8 |
9 | class UsersList extends Component{
10 | state = INITIAL_STATE;
11 | handleConfirm = () => {
12 | this.deleteUser();
13 | }
14 | handleCancel = () => {
15 | this.setState({showConfirm: false, selectedUser: null})
16 | }
17 | deleteUser = () => {
18 | serverAPI.delete(`/users/${this.state.selectedUser._id}`)
19 | .then(() => {
20 | this.setState({showConfirm: false, selectedUser: null});
21 | this.context.fetchUsersList();
22 | })
23 | .catch(err => console.log(err));
24 | }
25 | renderUsersList = () => {
26 | let users = this.context.users;
27 | if(!users || users.length < 1){
28 | return Loading users...
29 | } else {
30 | return users.map(user => {
31 | if(user.isAdmin){
32 | return
33 | }
34 | return (
35 |
36 |
37 | {user.firstName} {user.lastName}
38 |
39 |
40 | {this.setState({showConfirm: true, selectedUser: user})}}>
41 |
42 | Delete
43 |
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 |
--------------------------------------------------------------------------------