├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------