├── .gitignore ├── README.md ├── config-overrides.js ├── package-lock.json ├── package.json ├── public ├── dialpad.png ├── dtmf-tones.png └── index.html └── src ├── components ├── App.js └── KeypadKey.js ├── constants └── dtmf.js ├── index.js ├── store ├── audio │ ├── TouchTone.js │ ├── actions.js │ └── middleware.js └── index.js └── styles ├── StyledKeypad.js ├── StyledKeypadButton.js └── StyledKeypadRow.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db 38 | 39 | # Webstorm # 40 | ############ 41 | .idea 42 | .idea/* 43 | 44 | # Build # 45 | ######### 46 | build 47 | build/* 48 | 49 | # Node # 50 | ######## 51 | node_modules 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-dtmf-dialer 2 | 3 | ## What is this? 4 | A simple demo showing how to use React and Redux middleware to control a Web Audio API system. 5 | 6 | [DTMF](https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling) (Dual-tone multi-frequency) signaling is the 7 | audio protocol used by TouchTone telephones to dial numbers. In short, when a key on the keypad is pressed, two 8 | frequencies are emitted, according to the following table: 9 | 10 | ![DTMF Tones](public/dtmf-tones.png "DTMF Tones") 11 | 12 | We can simulate a TouchTone keypad using the 13 | [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) 14 | by wiring up a couple of 15 | oscillators and telling them which frequencies to play when we press a key. The 16 | question is how to interface such an audio system with a React/Redux application. 17 | 18 | With React/Redux, reducers generally handle actions that we dispatch from the UI, 19 | e.g., 'play these two tones'. However reducers are meant to manage serializable 20 | application state. A Web Audio system is not a serializable object and has no place 21 | being handled in a reducer. Instead, we handle such things with middleware. 22 | 23 | This demo and [the accompanying video](https://youtu.be/zps9YDPJha0) and [article](http://cliffordhall.com/2018/12/controlling-web-audio-with-react-and-redux-middleware/) demonstrate how to achieve that. 24 | 25 | ## Setup 26 | 27 | ### Install Node and npm 28 | [Node](https://nodejs.org/en/download/) 10.0 or above (also installs npm) 29 | 30 | ### Install Node modules 31 | ```cd path/to/react-dtmf-dialer``` (the rest of this document assumes you are at this location) 32 | 33 | ```npm install``` 34 | 35 | ## Launch 36 | 37 | The npm script to launch the application has been defined in ```package.json```. 38 | 39 | #### Inside your IDE 40 | If you're running a modern IDE like Webstorm, you can just open the npm window and double-click on each ```start``` script. 41 | 42 | #### From the command line 43 | 44 | ```npm run start``` 45 | 46 | Once that's done, open a browser window and navigate to ```http://localhost:3000/``` 47 | 48 | You should see the application keypad: 49 | 50 | ![DTMF Keypad](public/dialpad.png "Touch Tone Keypad") 51 | 52 | Click a key to hear that TouchTone goodness. 53 | 54 | ## Dependencies 55 | This React client uses: 56 | * [react-scripts](https://www.npmjs.com/package/react-scripts) for abstracting away the config of Babel, Webpack, and JSX 57 | * [redux](https://github.com/reduxjs/redux) to manage application state 58 | * [react-redux](https://github.com/reduxjs/react-redux) to inject the store's dispatch function and selected parts of the 59 | application state into any component's props. 60 | * [bootstrap](https://getbootstrap.com/) for UI components 61 | * [react-bootstrap](https://react-bootstrap.github.io/) for integration of bootstrap with react 62 | * [styled-components](https://www.styled-components.com/) to apply CSS for managing the layout and making the buttons nice and square 63 | * [react-app-rewired](https://github.com/timarney/react-app-rewired) for overriding react-scripts so styled-components can do its magic 64 | 65 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const rewireStyledComponents = require('react-app-rewire-styled-components'); 2 | 3 | module.exports = function override(config, env) { 4 | console.log("⚡⚡⚡ Overriding default Create React App Configuration! ⚡⚡⚡"); 5 | config = rewireStyledComponents(config, env); 6 | return config; 7 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dtmf-dialer", 3 | "version": "1.0.0", 4 | "description": "Play with the Web Audio API in React", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "react-scripts start", 8 | "build": "react-scripts build" 9 | }, 10 | "author": "Cliff Hall", 11 | "license": "MIT", 12 | "dependencies": { 13 | "bootstrap": "^3.3.7", 14 | "react": "^16.7.0", 15 | "react-bootstrap": "^0.32.4", 16 | "react-dom": "^16.7.0", 17 | "react-redux": "^6.0.0", 18 | "react-scripts": "^2.1.2", 19 | "redux": "^4.0.1", 20 | "styled-components": "^4.1.3" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 11", 26 | "not op_mini all" 27 | ], 28 | "devDependencies": { 29 | "react-app-rewire-styled-components": "^3.0.2", 30 | "react-app-rewired": "^1.6.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/dialpad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffhall/react-dtmf-dialer/c082015e984344f985b3124e556894a965a89d99/public/dialpad.png -------------------------------------------------------------------------------- /public/dtmf-tones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffhall/react-dtmf-dialer/c082015e984344f985b3124e556894a965a89d99/public/dtmf-tones.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React DTMF Dialer 5 | 6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux'; 3 | 4 | import {KEYPAD} from '../constants/dtmf'; 5 | import {playDTMFPair} from '../store/audio/actions'; 6 | import {StyledKeypad} from '../styles/StyledKeypad'; 7 | import {StyledKeypadRow} from '../styles/StyledKeypadRow'; 8 | import KeypadKey from './KeypadKey'; 9 | 10 | class App extends Component { 11 | 12 | render() { 13 | 14 | const {playTones} = this.props; 15 | 16 | return 17 | { 18 | KEYPAD.map( (row, rindex) => 19 | 20 | {row.map( key => )} 25 | ) 26 | } 27 | ; 28 | } 29 | } 30 | 31 | const mapDispatchToProps = (dispatch) => ({ 32 | playTones: tones => dispatch(playDTMFPair(tones)) 33 | }); 34 | 35 | export default connect(null, mapDispatchToProps)(App); 36 | -------------------------------------------------------------------------------- /src/components/KeypadKey.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {StyledKeypadButton} from '../styles/StyledKeypadButton'; 4 | 5 | export default function KeypadKey(props) { 6 | 7 | const {label, tones, handleClick} = props; 8 | 9 | return handleClick(tones)}>{label} 10 | 11 | } -------------------------------------------------------------------------------- /src/constants/dtmf.js: -------------------------------------------------------------------------------- 1 | // DTMF ROW FREQUENCIES 2 | const ROW_1 = 697; 3 | const ROW_2 = 770; 4 | const ROW_3 = 852; 5 | const ROW_4 = 941; 6 | 7 | // DTMF COLUMN FREQUENCIES 8 | const COL_1 = 1209; 9 | const COL_2 = 1336; 10 | const COL_3 = 1477; 11 | 12 | // DTMF KEY FREQUENCY PAIRS 13 | const KEY_1 = [ROW_1, COL_1]; 14 | const KEY_2 = [ROW_1, COL_2]; 15 | const KEY_3 = [ROW_1, COL_3]; 16 | 17 | const KEY_4 = [ROW_2, COL_1]; 18 | const KEY_5 = [ROW_2, COL_2]; 19 | const KEY_6 = [ROW_2, COL_3]; 20 | 21 | const KEY_7 = [ROW_3, COL_1]; 22 | const KEY_8 = [ROW_3, COL_2]; 23 | const KEY_9 = [ROW_3, COL_3]; 24 | 25 | const KEY_STAR = [ROW_4, COL_1]; 26 | const KEY_0 = [ROW_4, COL_2]; 27 | const KEY_POUND = [ROW_4, COL_3]; 28 | 29 | // DTMF KEYPAD LABELS AND FREQUENCY PAIRS 30 | export const KEYPAD = [ 31 | [ ['1', KEY_1], ['2', KEY_2], ['3', KEY_3], ], // KEYPAD ROW 1 32 | [ ['4', KEY_4], ['5', KEY_5], ['6', KEY_6], ], // KEYPAD ROW 2 33 | [ ['7', KEY_7], ['8', KEY_8], ['9', KEY_9], ], // KEYPAD ROW 3 34 | [ ['*', KEY_STAR], ['0', KEY_0], ['#', KEY_POUND] ] // KEYPAD ROW 4 35 | ]; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import 'bootstrap/dist/css/bootstrap.css'; 6 | import 'bootstrap/dist/css/bootstrap-theme.css'; 7 | 8 | import App from './components/App' 9 | import store from './store'; 10 | 11 | function render() { 12 | ReactDOM.render( 13 | 14 | , 15 | , 16 | document.getElementById('app') 17 | ); 18 | } 19 | render(); -------------------------------------------------------------------------------- /src/store/audio/TouchTone.js: -------------------------------------------------------------------------------- 1 | export default class TouchTone { 2 | 3 | constructor() { 4 | // Get the audio context 5 | this.context = new (window.AudioContext || window.webkitAudioContext)(); 6 | } 7 | 8 | init() { 9 | 10 | // Create, amplify, and connect row oscillator 11 | this.rowOscillator = this.context.createOscillator(); 12 | this.rowOscillator.type = 'sine'; 13 | this.rowGain = this.context.createGain(); 14 | this.rowOscillator.connect(this.rowGain); 15 | this.rowGain.connect(this.context.destination); 16 | 17 | // Create, amplify, and connect column oscillator 18 | this.colOscillator = this.context.createOscillator(); 19 | this.colOscillator.type = 'sine'; 20 | this.colGain= this.context.createGain(); 21 | this.colOscillator.connect(this.colGain); 22 | this.colGain.connect(this.context.destination); 23 | 24 | } 25 | 26 | play(tones) { 27 | 28 | // initialize 29 | this.init(); 30 | 31 | // Get the current time from the audio context 32 | const time = this.context.currentTime; 33 | 34 | // Load tones into oscillators 35 | this.rowOscillator.frequency.value = tones[0]; 36 | this.colOscillator.frequency.value = tones[1]; 37 | 38 | // Set gain and start oscillators 39 | this.rowGain.gain.setValueAtTime(1, time); 40 | this.colGain.gain.setValueAtTime(1, time); 41 | this.rowOscillator.start(time); 42 | this.colOscillator.start(time); 43 | 44 | // Set the stop time 45 | this.stop(time + .5); 46 | 47 | } 48 | 49 | stop(time) { 50 | 51 | // Ramp down gain and stop oscillators 52 | this.rowGain.gain.setValueAtTime(0, time); 53 | this.colGain.gain.setValueAtTime(0, time); 54 | this.rowOscillator.stop(time); 55 | this.colOscillator.stop(time); 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /src/store/audio/actions.js: -------------------------------------------------------------------------------- 1 | // Audio related actions 2 | export const PLAY_DTMF_PAIR = 'audio/play-dtmf'; 3 | 4 | // Play a DTMF tone pair 5 | export const playDTMFPair = tones => { 6 | return { 7 | type: PLAY_DTMF_PAIR, 8 | tones 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/store/audio/middleware.js: -------------------------------------------------------------------------------- 1 | import TouchTone from './TouchTone'; 2 | import {PLAY_DTMF_PAIR} from "./actions"; 3 | 4 | export const audioMiddleware = store => { 5 | 6 | const touchTone = new TouchTone(); 7 | 8 | return next => action => { 9 | 10 | switch (action.type) { 11 | 12 | case PLAY_DTMF_PAIR: 13 | touchTone.play(action.tones); 14 | break; 15 | 16 | default: 17 | break; 18 | 19 | } 20 | 21 | next(action); 22 | } 23 | 24 | }; -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware} from 'redux'; 2 | 3 | import {audioMiddleware} from './audio/middleware'; 4 | 5 | const store = createStore( 6 | () => {}, 7 | applyMiddleware(audioMiddleware)); 8 | 9 | export default store; -------------------------------------------------------------------------------- /src/styles/StyledKeypad.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledKeypad = styled.div` 4 | &&& { 5 | margin-top: 150px; 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | align-items: center; 10 | align-content: center; 11 | justify-content: center; 12 | flex-direction: column; 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /src/styles/StyledKeypadButton.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {Button} from "react-bootstrap"; 3 | import styled from "styled-components"; 4 | 5 | const StyledBootstrapButton = styled(Button)` 6 | &&& { 7 | width: 100px; 8 | height: 100px; 9 | font-size: 36px; 10 | outline: none; 11 | } 12 | `; 13 | 14 | export class StyledKeypadButton extends Component { 15 | render() { 16 | const {...props} = this.props; 17 | return 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/StyledKeypadRow.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledKeypadRow = styled.div` 4 | &&& { 5 | display: flex; 6 | flex-direction: row; 7 | } 8 | `; 9 | --------------------------------------------------------------------------------