W. Turner, S. Leonard - JavaScript for Sound Artists. Learn to Code With the Web Audio API (2023)
W. Turner, S. Leonard - JavaScript for Sound Artists. Learn to Code With the Web Audio API (2023)
Sound Artists
Learn how to program JavaScript while creating interactive audio applications with
JavaScript for Sound Artists: Learn to Code with the Web Audio API! William Turner and
Steve Leonard showcase the basics of JavaScript language programming so that read-
ers can learn how to build browser-based audio applications such as music synthesiz-
ers and drum machines. The companion website offers further opportunity for growth.
Web Audio API instruction includes oscillators, audio file loading and playback, basic
audio manipulation, panning, and time. This book encompasses all of the basic features
of JavaScript with aspects of the Web Audio API to heighten the capability of any browser.
Key Features
The context of teaching JavaScript for the creative audio community in this manner does not
exist anywhere else in the market and uses example-based teaching.
William Turner is a technical trainer with over 13 years of experience. He currently oper-
ates a boutique web development and training company at helpknow.com.
Steve Leonard is a technical writer for Juniper Networks and developed some of the ini-
tial documentation for the Cisco Nexus 7000 Series of switches among other products.
He now writes the internal programmer guide for developers of Juniper’s next-generation
operating system and is responsible for the network management documentation for end
users of that new OS.
JavaScript for
Sound Artists
Learn to Code with the Web Audio API
2nd Edition
Reasonable efforts have been made to publish reliable data and information, but the author and
publisher cannot assume responsibility for the validity of all materials or the consequences of
their use. The authors and publishers have attempted to trace the copyright holders of all material
reproduced in this publication and apologize to copyright holders if permission to publish in this
form has not been obtained. If any copyright material has not been acknowledged please write and
let us know so we may rectify in any future reprint.
Except as permitted under U.S. Copyright Law, no part of this book may be reprinted, reproduced,
transmitted, or utilized in any form by any electronic, mechanical, or other means, now known or
hereafter invented, including photocopying, microfilming, and recording, or in any information
storage or retrieval system, without written permission from the publishers.
For permission to photocopy or use material electronically from this work, access www.copyright.
com or contact the Copyright Clearance Center, Inc. (CCC), 222 Rosewood Drive, Danvers, MA
01923, 978-750-8400. For works that are not available on CCC please contact mpkbookspermis-
[email protected]
Trademark notice: Product or corporate names may be trademarks or registered trademarks and are
used only for identification and explanation without intent to infringe.
DOI: 10.1201/9781003201496
Typeset in MinionPro
by codeMantra
William Turner
Contents
Preface xvii
What Is a Program? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1
What Is JavaScript? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1
HTML, CSS, and JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .2
What Is a Web Application? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .3
What Is the Web Audio API? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .3
Setting Up Your Work Environment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .4
How to View in Browser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .6
How to Create Code Snippets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .6
Accessing the Developer Tools. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .7
Troubleshooting Problems and Getting Help . . . . . . . . . . . . . . . . . . . . . . . . . .7
vii
Documenting Your Code with Comments . . . . . . . . . . . . . . . . . . . . . . . . . . .14
Exploring Variables with an Oscillator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .14
console.log() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .15
String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .16
Built in String Methods. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .17
The Length Property . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .19
Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .19
How to Determine the Data Type of a Variable?. . . . . . . . . . . . . . . . . . . . . . .20
Examples of Arithmetic Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .20
Number to String Conversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .22
BigInt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .23
Arrays. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .23
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .25
viii Contents
For Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .39
Using For Loops with Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .40
While Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .41
When to Use for Loops and When to Use While Loops . . . . . . . . . . . . . . . .42
Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .42
Block Scoped Variables Using Let and Const . . . . . . . . . . . . . . . . . . . . . . . . .42
Non-Block Scoped Variables Using Var . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .43
Local and Higher Scope Access . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .43
Hoisting and Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .44
Hoisting with Let and Const . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .45
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .45
Contents ix
The “this” Keyword . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .72
The bind Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .72
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
x Contents
Programming the Frequency Slider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .108
Changing the Frequency in Real Time. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .109
Changing Waveform Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
Completed Code with Waveform Selection. . . . . . . . . . . . . . . . . . . . . . . . . .112
Giving an Outline to the Selected Waveform Type . . . . . . . . . . . . . . . . . . .113
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
Contents xi
The Two Steps to Loading an Audio File . . . . . . . . . . . . . . . . . . . . . . . . . . . .128
Loading an Audio File with the XMLHttpRequest Object . . . . . . . . . . . . .128
get Requests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .130
A Word on Audio File Type Compatibility . . . . . . . . . . . . . . . . . . . . . . . . . .130
Synchronous versus Asynchronous Code Execution. . . . . . . . . . . . . . . . . .130
Playing the Audio Content . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .132
Processing the Audio Buffer with the Node Graph . . . . . . . . . . . . . . . . . . .132
Loading Audio Files Using the Fetch API . . . . . . . . . . . . . . . . . . . . . . . . . . .133
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .134
xii Contents
Some Effects Require Development Work . . . . . . . . . . . . . . . . . . . . . . . . . . .159
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .160
Contents xiii
xiv Contents
CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .221
Adding Interactivity to the Grid Elements . . . . . . . . . . . . . . . . . . . . . . . . . .223
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .225
Index 251
Contents xv
Preface
xvii
explained at all). We assume that you are either familiar with many of these core
audio concepts or know enough to find the answers on your own. If you need
an accommodating audio technology reference, we suggest David Miles Huber’s
excellent book Modern Recording Techniques, Taylor & Francis.
This book is also not directed toward experienced programmers who are simply
interested in JavaScript or the Web Audio API. If this describes you, then you
may find some value here, but you are not the intended audience.
Make Connections
Generally, it is easier to learn new things by making associations and connec-
tions to areas that you are already familiar with. If you have ever programmed a
synth or a MIDI sequencer, then you have already done a form of programming.
The contents of this book are designed to be a bridge that connects a world you
are (presumably) familiar with (sound and audio technology) to a topic you are
less familiar with—JavaScript and programming. We suggest that you tap into
whatever has drawn you to sound art while learning the material in this book.
Make It Habitual
Programming is all about learning a bunch of little things that combine to make
big things. The best way to learn a lot of little things is through repetition and
habit. One way to do this is to simply accept programming as a new part of your
lifestyle and do a little bit (or a lot) every day.
xviii Preface
Keep Going
Our final piece of advice is to simply stick with it.
Best of luck!
If you have any questions or comments, you can find us at:
http://www.javascriptforsoundartists.com
William Turner
Steve Leonard
Preface xix
1 Overview
and Setup
What Is a Program?
A program is any set of instructions that are created or followed. In this book, we
focus on writing computer programs, which are lists of instructions that a com-
puter carries out. These instructions can be written and stored in various forms.
Some of the first modern computers used punched cards, switches, and cables.
Early analogue music synthesizers were a type of computer that used a patchbay
style interface to manually allow a programmer to create specific sounds.
What Is JavaScript?
JavaScript is a multipurpose programming language initially created to aid
developers in adding dynamic features to websites. The language was initially
created in 11 days and released in 1995 by a company called Netscape. Developed
by Brendan Eich, its original release name was LiveScript. When Netscape intro-
duced support for the language in its browser, LiveScript was renamed JavaScript.
Although JavaScript is similar in name to the Java programming language, they
are completely unrelated. Today, JavaScript is used in everything from robotics
to home automation systems.
DOI: 10.1201/9781003201496-1 1
HTML, CSS, and JavaScript
The three main technologies used to build websites and web applications are
HTML, CSS, and JavaScript.
HTML stands for hypertext markup language and is the standard by which
we create documents for the World Wide Web. You program HTML by writing
elements (sometimes referred to as tags for brevity). These elements contain text
and other nested elements, which make up the document’s content.
CSS stands for cascading style sheets and is a tool used to modify how HTML
elements and text are presented. CSS is primarily a visual design tool. For exam-
ple, with CSS you could modify an HTML element and give it an orange back-
ground, change its font size, place it vertically or horizontally, or perform any
number of creative visual changes.
5. Inside the web audio template folder, create another folder called css.
6. In Sublime Text, create a new file by going to the File menu and click
New. Save this file in your css folder as app.css. Leave the contents of this
file empty.
7. In the web audio template folder, create another folder called js.
You are now going to add a few extensions to Sublime Text that will make working
with the editor easier in the long term. To do this, you must first download and install
the package manager plug-in. Go to the following link and follow the directions on
the left side of the window: https://packagecontrol.io/installation. When done, close
the console by entering the keys: Ctrl + ` (apostrophe, on the key with the ~).
1. In the Sublime Text menu, go to Tools > Command Palette, and in the
form field that appears, type install. You should see an option menu
appear that says Package control: Install package. Click this menu option.
2. In the window that appears, delete everything on line 3 and paste the
following text: This is a test snippet.
We are not going to go over the utility of the developer tools just yet, but they will
be highlighted throughout the book.
DOI: 10.1201/9781003201496-2 9
After you verify that the Hello Sound program works, close the browser. You
just wrote your first Web Audio API program!
The code you just ran is a basic oscillator generation and playback script.
The first line in the script is called the “Audio Context” and this tells the browser
that you are using the Web Audio API. The next line of code creates an oscil-
lator. The third line of code assigns a waveform type to the oscillator, whereas
line four connects the oscillator to a virtual audio output called the destination,
which is analogous to the speakers of your computer. The last line starts the
oscillator playing.
If you were to run the previous code on a public web server (as opposed to
privately on your local web server), it will not play unless the user enables the
web audio API via a gesture such as a mouse click or other action. For this, we
will need to write additional code. To see a warning about this problem, refresh
the browser and open the developer tools. A warning stating “The AudioContext
was not allowed to start. It must be resumed (or created) after a user gesture on
the page” is presented.
The code to fix the error is presented below. Remove the previous code and
replace it with the following code.
const audioContext = new AudioContext();
document.addEventListener("mousedown", function() {
let osc = audioContext.createOscillator();
osc.type = "sine";
osc.connect(audioContext.destination);
osc.start(audioContext.currentTime);
});
When you launch the web server and click the webpage an oscillator will play.
The code used to trigger the oscillator when the page is clicked is called an event
listener. The event listener is the portion of code that looks like this:
document.addEventListener("mousedown", function() {
In the example below, all web audio code except the AudioContext is placed
inside the event listener.
const audioContext = new AudioContext();
document.addEventListener("mousedown", function() {
let osc = audioContext.createOscillator();
osc.type = "sine";
});
A more sophisticated version of the above code is written below. The following
code lets you toggle the oscillator on and off when the page is clicked:
const audioContext = new AudioContext(); //___Initializes web audio
api
let osc;
function playOsc(oscType, freq) {
if(osc){
osc.stop(audioContext.currentTime);
console.log("Stopped");
osc = undefined;
}else{
osc = audioContext.createOscillator();
osc.type = oscType;
osc.frequency.value = freq;
osc.connect(audioContext.destination);
osc.start(audioContext.currentTime);
console.log("Start")
}
}
document.addEventListener("mousedown", function() {
playOsc("sine", 330);
});
Don’t worry about knowing how the code works just yet; we are going to cover
detailed operation of the Web Audio API in future chapters. First, though, we
need to cover the basics of the JavaScript language.
Variables
One of the first steps in writing a program is understanding variables and variable
assignment. Variables are word forms that are used to store data. For example:
let waveformType = "sawtooth";
The variable here is named waveformType. This is preceded by the let keyword.
The let keyword is a variable declaration keyword. You always specify the vari-
able declaration keyword prior to declaring the variable. Declaring a variable
means you are creating a new variable. JavaScript has three different variable
declaration keywords and they are const, let, and var. In this chapter, we go over
the let and const declaration and in later chapters you learn about var. After typ-
ing the declaration keyword you type a space and give a name to your variable.
Variable names are typically a reflection of the thing they represent. In this case,
the variable is being used to describe a type of oscillator waveform, so is named
waveformType. You probably noticed the odd capitalization of the word “type” in
waveformType. The convention of capitalizing words to distinguish them within
variable names is called camel case. This convention is used because variable
Variables 11
names cannot contain white space to separate them. If you rewrote the variable
in the following manner, you get an error:
let waveform type = "sawtooth"; //____returns an error
Type the above code into the app.js file of your hello_sound template. Launch
the browser and open the developer tools by right clicking on the page and
selecting “inspect” or using the key command (Windows: Ctrl+Shift+I or Mac:
Command+Option+I). Inside the console tab you should see an error similar to
the one in the following image.
The text in red is the actual error and is identified as a syntax error. Directly to
the right of the error you can see the file and the line number where the error
occurred. This number corresponds to the line number in your file, which might
differ from the one in the image. After you see the error, remove the line you
added that is causing the error in app.js and save the file.
After you declare and name a variable, you can assign some data to it. You
use the assignment operator “=” to do this.
It is important to understand that in JavaScript the “=” symbol is not called
the “equal sign” and its functionality does not mean equal to. The “=” symbol
indicates assignment, so it is called the assignment operator. The value on the
right side of the assignment operator contains the data you want to assign to the
variable name on the left side. In the following example, the string “sawtooth” is
assigned to the variable waveformType:
let waveformType = "sawtooth";
When you assign a string of words to a variable, you must place them between
quotation marks. The resulting data type is called a string. Data types represent
the types of data that you can use in your program. Different programming lan-
guages have different data types. JavaScript has eight data types and one of these
is the string data type (see Chapter 6 for a list of JavaScript data types).
After you assign data to your variable, you must end the variable declaration
with a semicolon.
In summary, there are five parts to a variable declaration:
You can assign multiple variables at once using the following syntax:
let osc1 = 1200,
osc2 = 1300,
osc3 = 100;
When using the let keyword to declare variables you can reassign them as in this
example.
let osc = 500;
console.log(osc) // 500
osc = 1000;
console.log(osc) // 1000
Declaring variables with let more than once in the same scope will create an
error. You will learn more about scope in later chapters.
let osc = 500;
let osc = 1000; // error, variable already declared.
The const declaration behaves differently than let. Const is used with variables
that the developer intends to remain unchanged after being declared. If you
declare a variable with const and assign it a primitive data type such as a number
or string you will get an error if you change it.
const osc = 1000;
osc = 1000 // reassignment gives an error
In some cases, you might want to declare a variable and not assign data to it, as
in the following example:
let waveformType;
The keyword undefined is another JavaScript data type. Notice that undefined
is not enclosed in quotation marks because it is not a string but represents a data
type.
If you declare a variable using const and do not explicitly assign a value to it,
the interpreter will return an error.
const osc; // SyntaxError: Missing initializer in const declaration
Variables 13
Null
The primitive value null is similar to the primitive value undefined. Both can act
as a placeholder for empty variables. When the type of operator (discussed later
in this chapter) is used to determine the type of null, the result is object. This is
not what you might expect and is a flaw in the language. The correct returned
value should be null. Because of this, I suggest you never use null and always use
undefined.
All the characters from the // to the end of the line are ignored by the computer.
Replace the osc.type assignment with the waveformType variable like this:
const audioContext = new AudioContext();
let waveformType = "sawtooth"; //___added variable
let osc = audioContext.createOscillator();
osc.type = waveformType; //__Assigned it to our oscillator type
osc.connect(audioContext.destination);
osc.start(audioContext.currentTime);
Each of the four new variables contains a string that represents an oscillator
waveform type. The square variable is assigned to the currentWaveform variable
in the following line:
currentWaveform = square;
console.log()
When programs begin to get big, it can be difficult to know what value is assigned
to a variable at any given moment. One way you can find out is by using a built-in
feature called console.log().
The way you do this is by typing console.log() into your code at the point
where you want to check a given variables assignment. You then place the vari-
able name inside the parenthesis. You can also add a message to console.log() by
placing a comma after the variable and writing a message in quotes.
To see what the currentWaveform variable has as its assignment, you do this:
const audioContext = new AudioContext();
//_____________Added 4 variables that represent oscillator
waveforms
let saw = "sawtooth";
let sine = "sine";
let tri = "triangle";
let square = "square";
console.log() 15
let currentWaveform = undefined;
currentWaveform = square;
console.log(currentWaveform, “The waveform type”); //___ square
//____________________________________Start of oscillator
let osc = audioContext.createOscillator();
osc.type = currentWaveform; // Assigned it to our oscillator type
osc.connect(audioContext.destination);
osc.start(audioContext.currentTime);
If you launch Brave, open the developer tools and click the console tab, you will
see the output of our console.log().
One thing to remember is that because variables can have different values
at different times, the output of console.log() depends on where it is placed in
the program. If you modify the last example and place console.log() immedi-
ately after the currentWaveform variable which has undefined assigned to it, then
undefined is output to the log.
//__________A variable intended to contain one of these waveforms
let currentWaveform = undefined;
console.log(currentWaveform); //______results in "undefined"
currentWaveform = square;
So far we’ve mentioned three of the seven data types in JavaScript. The first was
string, the second was undefined, and the last was null.
Before we go further let’s explore the string data type a bit more in depth.
String
As we already discovered, strings are denoted by quotation marks. The variable
below is a string:
let oscillator = "square";
You can manipulate strings in different ways. One of the most common is by
combining multiple strings into one string. This is called concatenation and it
works by using the plus sign (+) like this:
let oscillator = "saw" + "tooth";
console.log(oscillator); // sawtooth
Another way to combine strings is with string interpolation using template literals.
A template literal uses the “${}” syntax. For template literals, back ticks are used to
create a string and variables are placed in the curly braces after a dollar sign.
If you want to get the number of characters in a string, you can use what is called
the length property like this:
console.log(myFavoriteSynthCompany.length); // 33
The output of the length property includes the white-space characters of the
string.
toUpperCase()
This method changes all the characters in a string to uppercase.
let oscillator = "sawtooth";
oscillator.toUpperCase(); // SAWTOOTH
toLowerCase()
This method changes all the characters in a string to lowercase.
let oscillator = "SAWTOOTH";
oscillator.toLowerCase(); // "sawtooth"
You do not need to immediately memorize how each of these string methods
works, but it is a good idea to know about them. This way, when you do need to
implement any of the functionality they provide, you know which tool to reach
charAt()
This method gets a character at any given index value within a string. For exam-
ple, if you have the string “oscillator-1” and want to know what the second letter
of this string is without actually looking at it, you can do this:
let sound = "oscillator";
console.log(sound.charAt(1)); // "s"
Now you might be wondering why charAt(1) would give us back “s” and not “o”.
The reason is that the count begins at zero. So if we wanted the first letter we
would do this:
console.log(sound.charAt(0)); // "o"
replace()
This method finds a group of characters in a string and replaces them with
another string. If you want to replace an entire word you can do it like this:
let myFavoriteSynthCompany = "My favorite synth company is Moog.
Moog is great!";
let myNewFavoriteSynthCompany = myFavoriteSynthCompany.
replace("Moog","Dave Smith Instruments");
console.log(myNewFavoriteSynthCompany); // My favorite synth
company is Dave Smith Instruments. Moog is great!
As you probably noticed, when using the replace method in this manner it
only replaces the first instance of the word you select. To replace all instances
of the word, you need to use the following syntax to globally replace them in
the string.
let myFavoriteSynthCompany = "My favorite synth company is Moog.
Moog is great!";
let myNewFavoriteSynthCompany = myFavoriteSynthCompany.replace(/
Moog/gi,"Dave Smith Instruments");
console.log(myNewFavoriteSynthCompany); // My favorite synth company
is Dave Smith Instruments. Dave Smith Instruments is great!
The g stands for global and the i denotes case insensitivity. If you wanted the
string replacement to be case-sensitive you would simply use a g and omit the i.
These characters are part of a pattern matching language for string data called
regular expressions. Regular expressions are an advanced topic that will not be
covered further in this book.
Like charAt(), slice() works on a zero-based index. This means the first character is
always 0. The slice method takes two values: a beginning index value and an ending
index value. When a method takes values they are called arguments. The charAt()
method takes one argument. The slice() method takes two arguments. The slice
method’s first argument is where the slice starts, and this value is included in the
slice. The second value is where the slice ends and is non-inclusive. This means all
the characters up to but not including the second value are included in the slice.
If you want to get the last value of a string, you can combine the length property
with the charAt() method. This allows you to retrieve the last character in a string
in a manner that doesn’t require you to know how long the string is. The code
shows an example of this. The reason you subtract 1 from the length property is
because the length property begins counting at one while charAt() begins count-
ing at zero. Therefore, you subtract one from the length property to compensate
for the offset.
let sound = "oscillator-1";
let oscNumber = sound.charAt(sound.length − 1);
console.log(oscNumber); // 1
Numbers
In JavaScript, numbers are a distinct data type. Below is a variable named fre-
quencyValue and it is assigned a number of 200. It is then assigned to the oscil-
lators pitch. If you place the code below in a new JavaScript file and run it, you
will hear an oscillator play at a frequency of 200 Hz. Modify the number value
assigned to the frequencyValue variable and launch the code to hear the oscilla-
tor play at different pitches.
const audioContext = new AudioContext();
let frequencyValue = 200; //___Create variable frequencyValue
let waveform = "sawtooth";
Numbers 19
let osc = audioContext.createOscillator();
osc.type = waveform;
//_____ assign it to the oscillators pitch
osc.frequency.value = frequencyValue;
osc.connect(audioContext.destination);
osc.start(audioContext.currentTime);
Unlike strings, numbers do not use quotation marks. In fact, if you did use a num-
ber with quotation marks its data type would not be number, it would be string.
Here’s an example:
let oscillators = " 6 ";
let polyphony = 6;
console.log(typeof oscillators); // string
console.log(typeof polyphony); // number
You can do basic math with numbers using the following symbols. These symbols
are called arithmetic operators.
+ Addition
− Subtraction
* Multiplication
/ Division
% Modulo
The last symbol (%) might be new to you and it’s pronounced moj-uh-loh. The
purpose of this symbol is to output the remainder of a division. So for example:
console.log(12 % 9); // This would equal 3
The precedent rules of algebra also apply. If you wrap a calculation in parenthesis,
the calculation inside the parenthesis is performed first.
If you want to do more elaborate calculations, JavaScript has a built-in tool called
the Math object which allows you to use a collection of math methods to manipu-
late numbers.
So, for example, if we wanted to round a decimal number to its nearest inte-
ger, you can use
Math.round() like this:
Math.round(1000.789) // outputs 1001
Let’s go over each of these one by one. If you would like to explore more math
methods, a good site is the Mozilla Developer Network at: https://developer.
mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math.
Math.random()
The random method creates a random number between zero and one.
let randomNumber = Math.random();
console.log(randomNumber); // example: 0.019790495047345757
Math.abs()
The abs method allows you to get the absolute value of a number.
let num = Math.abs(−100);
console.log(num); // 100
This is useful for finding the difference between numeric variables of unknown
values.
let a = 1000;
let b = 5000;
console.log(Math.abs(b − a)); // 4000
If you attempt to do a math operation using non-numeric values, sometimes you will
receive a returned value of NaN. This stands for not a number. Here is an example
of attempting to add two values in which one value is a number and the other is not.
let osc1 = undefined;
let osc2 = 200;
console.log( osc1 + osc2 ); // NaN
BigInt
As of this writing, the newest JavaScript data type is BigInt. BigInt lets you work
with integers that are larger than what is allowed using the number data type. To
demonstrate the problem it solves, look at the following code:
console.log(9999999999999999); // 10000000000000000
The behavior above is called integer overflow and it happens when you attempt to
create a number that exceeds what JavaScript can represent by a given number of
digits. If you need to work with very large integers, you can use BigInt to repre-
sent them. The syntax to convert a number to a BigInt requires that you append
the letter “n” to the number that you want to convert.
let someLargeInteger = 9999999999999999n;
// to inspect the value you need to use the toValue() method
console.log(someLargeInteger.valueOf()) // 9999999999999999n
Basic arithmetic operators work on BigInt and Math methods do not. Working
with BigInts is an advanced topic and when working with numbers we will be
using the number data type for the rest of the book.
Arrays
Arrays are a construct that holds multiple pieces of data. You can think of them
as variables that hold more than one item. Arrays are expressed using brackets,
where each item is separated by a comma. Each item in the array is designated an
index number with the first item starting at zero.
let waveforms = [ ]; // empty array
let waveforms = ["square","sawtooth","triangle","sine"]; // array
with some data
If you want to access any of this data, you can use the following notation:
waveforms[0]; // square
waveforms[1]; // sawtooth
Arrays 23
waveforms[2]; // triangle
waveforms[3]; // sine
waveforms[4]; // undefined ( no data )
If you want to know how many items are inside an array, use the length property
like this:
let waveforms = ["square", "sawtooth", "triangle", "sine"];
waveforms.length; // 4
Arrays come with built-in methods that you can use to manipulate the data in
them. A full list of these is available at the Mozilla developer network at this
URL: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
Global_Objects/Array. We are only going to go over a handful of these and
they are:
push()
This method adds items to the end of an array.
let synthFrequencies = [5000, 1000, 500];
synthFrequencies.push(100); // This places a new item at the end of
the array
console.log (synthFrequencies); // [5000,1000,500,100]
You can use the push method to add multiple items at once.
let synthFrequencies = [5000, 1000, 500];
synthFrequencies.push(100, 50, 30);
console.log(synthFrequencies); // [ 5000, 1000, 500, 100, 50, 30]
pop()
This method removes a single item at the end of an array.
let synthFrequencies = [5000, 1000, 500];
synthFrequencies.pop();
console.log(synthFrequencies); // [ 5000, 1000]
If you want to capture the last item you removed from an array in a variable,
do this:
let synthFrequencies = [5000, 1000, 500];
let lastItem = synthFrequencies.pop();
console.log(lastItem); // 500
If you want to capture the first item you removed from an array in a variable,
do this:
let synthFrequencies = [5000, 1000, 500];
let firstItem = synthFrequencies.shift();
console.log(firstItem); // 5000
unshift()
This method adds new items to the beginning of an array.
let synthFrequencies = [5000, 1000, 500];
synthFrequencies.unshift(7500, 6000);
console.log(synthFrequencies); // [ 7500, 6000, 5000, 1000, 500 ];
concat()
This method merges multiple arrays together into one array.
let drumMachines = ["MPC", "Maschine", "TR 808"];
let keyboards = ["Juno", "ARP", "Jupiter"];
let percussion = ["vibraphone", "bongos"];
let stringed = ["guitar", "bass", "harp"]
let instruments = drumMachines.concat(keyboards, percussion,
stringed);
console.log(instruments); /* [ 'MPC','Maschine','TR 808','Juno','AR
P','Jupiter','vibraphone', 'bongos','guitar','bass','harp' ] */
A tool that you use to copy the content of an array into another array is the destruc-
turing assignment syntax. The syntax consists of three dots and is used like this:
let drumMachines = ["MPC", "Maschine", "TR 808"];
let instruments = [...drumMachines,
"Juno", "ARP", "Jupiter",
"vibraphone", "bongos",
"guitar", "bass", "harp"
]
console.log(instruments) /* [ 'MPC','Maschine','TR 808','Juno','ARP
','Jupiter','vibraphone', 'bongos','guitar','bass','harp' ] */
Summary
In this chapter, you learned about variables, comments, numbers, strings, and arrays.
In the next chapter, you will learn about various assignments and logical operators.
Summary 25
3 Operators
You learned about the basic assignment operator (=) and some of the arithmetic
operators in the previous chapter. In this chapter, we are going to explore other
assignment operators, as well as comparison operators, that allow you to deter-
mine the relationship between variables and values, such as whether they have
the same value. We will also explore the Boolean data type, which has either a
true or a false value that can be assigned to variables or is the result of a compari-
son operation.
DOI: 10.1201/9781003201496-3 27
assignment operators are used to assign values to variables. The logical opera-
tors are used to compare two values and return a true or false value based on the
result of the comparison.
Assignment Operators
Assignment operators are used to assign data to variables. Here is a list of assign-
ment operators:
Assignment
This operator assigns a value to a variable.
let osc = 100;
With assignment operators, you can also assign variables to other variables.
let osc1 = 100;
let osc2 = osc1;
console.log(osc2); // 100
Addition Assignment
This operator increments a numeric variable or appends a string to a variable.
In the following example, an oscillator is assigned a value of 100 and then incre-
mented by 100 to give it a value of 200.
let osc = 100;
osc += 100;
console.log(osc); // 200
To demonstrate the use of the addition assignment operator, the following code
sets an ever-increasing frequency change to an oscillator and you can listen to the
effect. A method called setInterval() is defined, although the specifics of setInter-
val() are not important at this time. What is important is understanding that the
addition assignment operator is incrementing the frequency value by 100 every
0.5 seconds when setInterval() is called.
let audioContext = new AudioContext();
let osc = audioContext.createOscillator();
osc.frequency.value = 300;
28 3. Operators
osc.connect(audioContext.destination);
osc.start(audioContext.currentTime);
setInterval(function(){
osc.frequency.value += 100; //____Increment frequency value by 100
every 0.5 seconds
console.log(osc.frequency.value);//_____View change
},500);//________________________500 milliseconds is 0.5 seconds
When you use the addition assignment operator with a string, the string you sup-
ply is concatenated with the variable. Here is an example:
let keyboards = "";
keyboards += "Korg ";
keyboards += "Yamaha ";
keyboards += "Kurzweil ";
console.log(keyboards); // Korg Yamaha Kurzweil
Subtraction Assignment
This operator is used to decrement a numeric variable.
let osc = 500;
osc –= 100;
console.log(osc); // 400
Multiplication Assignment
This operator multiplies a variable with a value and assigns it to the variable.
let osc = 200;
osc *= 2;
osc *= 2;
console.log(osc); // 800
Division Assignment
This operator divides a variable by a value and assigns it to the variable.
let osc = 200;
osc /= 2;
osc /= 2;
console.log(osc); // 50
Modulo Assignment
This operator divides a variable by a value and assigns the remainder of that divi-
sion to the variable.
let osc = 200;
osc %= 150;
console.log(osc); // 50
Modulo Assignment 29
The Boolean Data Type
The Boolean data type is either true or false. This is conveyed by the word-form
values true and false. Booleans are important because you can use them to pro-
gram on or off (true or false) values into the code. So, for example, you could use
them as a value that toggles an oscillator on or off. Assigning a Boolean value to
a variable in JavaScript looks like this:
let oscillatorIsOn = true; // true
oscillatorIsOn = false; // changed to false
Boolean values can also be the result of the comparison operators described
below or used in conditionals statements, which we will cover in the next chapter.
Comparison Operators
Comparison operators are used to compare two variables or values. They output
a true or false value depending on whether the variables or values are similar or
different from one another in some way. The similarity or difference being tested
for is dependent on the operator used. So, for example, if you test whether two
values are the same using the strict equality operator (===) and they are not the
same, the resulting value is false. There are eight comparison operators.
Equality Operator
This operator checks whether the left operand is equal to the right operand. It
then returns a Boolean value to represent the outcome of the comparison.
200 == 200; // true
"200hz" == "200hz"; // true
let osc1 = "200hz";
let osc2 = "200hz";
console.log(osc1 == osc2); // true
The equality operator can be a bit tricky because it attempts to do a data type
coercion before comparing operands. Data type coercion occurs when the code
interpreter (in our case the web browser) attempts to convert one data type into
another. In the following example, we compare a number and a numeric string.
30 3. Operators
JavaScript tries to convert the string to a number before doing the comparison.
If the string is a numeric string, the conversion is successful, and the compari-
son is performed. In this case, the result of the comparison is the Boolean value
true because the numeric string “200” is successfully converted to the value 200,
which matches the value of osc1.
let osc1 = 200;
let osc2 = "200";
console.log(osc1 == osc2); // true
200 == "oscillator" // false
The greater than and less than operators do data type coercion as shown in this
example:
600 > "500" // true
600 < "500" // false
The less than or equal to operator does data type coercion as shown in these
examples:
300 <= "300" // true
300 <= "500" // true
300 <= "200" // false
The not equal to operator does data type coercion as shown in this example:
"300" != 300 // false
Logical Operators
Logical operators allow you to check if a collection of statements is true or false
and return a Boolean value based on this information.
32 3. Operators
Logical Operator Name
&& AND
|| OR
! NOT
Another way to look at this code is that if a value is not false, then it is true, and
if its value is not true, then it is false.
In JavaScript, there are six values that evaluate to false. They are the following:
false
""
null
undefined
0
NaN
Summary
In this chapter, you learned about JavaScript assignment and logical operators,
the Boolean data type, and what values evaluate to false. In the next chapter, you
will learn to leverage these tools using two new concepts: conditionals and loops.
34 3. Operators
4 Conditional
Statements,
Loops, and
Scope
Conditional statements and loops are two of the most widely used constructs
in programming. Conditional statements allow your program to make choices
based on a set of criteria. Loops use repetition allowing your program to com-
plete many tasks quickly. Scope determines the availability of variables to differ-
ent parts of your code.
Conditional Statements
To create programs that do more than basic calculations or print text, they must
be able to make decisions. You can program these decisions by using conditional
statements. Conditional statements check if a value is true or false and then exe-
cute a branch of code based on this condition. We are going to go over the follow-
ing three conditional statements:
◾ if
◾ switch
◾ ternary
DOI: 10.1201/9781003201496-4 35
The If Statement
The syntax of an if statement consists of the if keyword, a pair of parenthesis,
and two curly braces. This is what an empty if statement looks like:
if(){
}
To use an if statement, you place a value or condition inside the parenthesis and
some code to execute inside the curly braces. If the condition inside the paren-
thesis evaluates to true, the code inside the curly braces is executed. If the condi-
tion evaluates to false, no action is taken and the code inside the curly braces is
skipped. In the following code, an if statement is used to check if an oscillator
frequency is set to 80 Hz prior to play start. If it is, the oscillator plays; if it is not,
the code inside the curly braces is ignored and the oscillator does not play.
//___________________________________BEGIN Setup
const audioContext = new AudioContext();
let osc = audioContext.createOscillator();
osc.type = "sawtooth";
osc.frequency.value = 80;
osc.connect(audioContext.destination);
//___________________________________END Setup
//___________________________________BEGIN Check frequency
if(osc.frequency.value === 80){
osc.start(audioContext.currentTime);
}
//___________________________________END Check frequency
If statements can also have an optional else branch that executes if the ini-
tial condition evaluates to false. In the following code, the if statement checks
to see if frequency.value is 100 Hz. If this condition is true, the oscillator
begins playing. If this condition is false, the else branch executes, assigns fre-
quency.value to 50 Hz, and starts the oscillator playing.
//____________________________________BEGIN Setup
const audioContext = new AudioContext();
let osc = audioContext.createOscillator();
osc.type = "sawtooth";
osc.frequency.value = 200;
osc.connect(audioContext.destination);
//____________________________________END Setup
//____________________________________BEGIN Conditional
if(osc.frequency.value === 100){
//__evaluates to false
osc.start(audioContext.currentTime);
}else{
//__So this plays
osc.frequency.value = 50;
osc.start(audioContext.currentTime);
Suppose you want to check for more than two conditions and do something dif-
ferent for each one. You can do this by creating an if statement with multiple
else if branches in sequence. The final else statement catches all conditions
that were not met along the way. An empty example looks like this:
if(){
}
else if(){
}
else{
}
In the following working example, the code executes and checks to see if osc.
type is set to “sine”. If this condition evaluates to false, the else if branch runs
and checks if the oscillator type is set to “sawtooth”. This evaluates to true
and the oscillator starts playing. If osc.type is not set to “sine” or “sawtooth”
(in other words, if both conditions are evaluated to false), then the result is the
execution of console.log(), which outputs “no condition met”.
let audioContext = new AudioContext();
let osc = audioContext.createOscillator();
osc.type = "sawtooth";
osc.connect(audioContext.destination);
if(osc.type === "sine"){
osc.start(audioContext.currentTime);
}else if(osc.type === "sawtooth"){
osc.frequency.value = 50;
osc.start(audioContext.currentTime);
}else{
console.log("no condition met");
}
Ternary Operator
If you are writing a conditional statement that contains a single compari-
son clause (it returns only one of two conditions), then you can use a ternary
operator. The ternary operator has three parts: an expression and two executed
statements. The first part is an expression that is tested for true or false and is
separated from the executed code by a question mark. If the expression evalu-
ates to true, the code to the left of the colon is run. If the expression evaluates to
false, the code to the right of the colon is run. The syntax of the ternary operator
looks like this:
/*
expression ? if true run this code : if false run this code
*/
Loops
Computers are very good at doing lots of simple tasks very fast. One of the tools
available to leverage this capability is loops. Loops allow you to repeat a task until a
condition or set of conditions are met. We will cover the following types of loops:
◾ for
◾ while
For Loops
The following code is an example of a for loop that counts to 16 and outputs
each loop number to the console. The text that follows explains the keywords and
what each component of the for loop does.
for(let i = 0; i <=16; i+=1){
console.log(i)
}
A for loop consists of the for keyword and an opening and closing parenthesis.
Inside the parenthesis are three parts separated by semicolons. The first part is
the initialization variable, and in this case it is set to 0.
for(let i = 0; i <=16; i+=1){
console.log(i);
}
The next part is the conditional statement and is used to determine a condition to
check upon each iteration of the loop. As long as this condition is true, the loop will
iterate (run another time). In the following example, the condition tells the for loop
to continue iterating as long as the value of the variable i is less than or equal to 16.
for(let i = 0; i <=16; i+=1){
console.log(i)
}
For Loops 39
The next part is used to increment the initialization variable. On each loop the
variable i is incremented by 1 and eventually reaches 17 and stops looping.
for(let i = 0; i <=16; i+=1){
console.log(i)
}
The last part of a for loop is the code block that is defined by the opening and
closing curly braces. Any code that is written in between these curly braces gets
repeated for each loop iteration.
for(let i = 0; i <=16; i+=1){
console.log(i) // code here gets repeated for each loop
}
When for loops are run they are very fast. Below is a script that uses an additional
helper function to pause each iteration of a for loop. The loop modifies the fre-
quency of a playing oscillator on each iteration. The helper function pauses the
loop (which is its only function) so you can hear each change.
/*__________________________________BEGIN Helper function.
Ignore this code it is simply being used to pause the for loop */
function sleep(milliseconds) {
let start = new Date().getTime();
for (let i = 0; i < 1e7; i++) {
if ((new Date().getTime() - start) > milliseconds){
break;
}
}
}
//__________________________________END Helper function
//__________________________________BEGIN Web Audio API setup
let audioContext = new AudioContext();
let osc = audioContext.createOscillator();
osc.type = "sawtooth";
osc.frequency.value = 30;
osc.connect(audioContext.destination);
osc.start(audioContext.currentTime);
If you want to modify each value in an existing array, you can do so by looping
through the array and modifying the value at each iteration. To do this, set the
value of the conditional statement termination value to the length of the array.
In the following code, this is done with synths.length. You can then access
the individual values of the array within the loop by placing the iterator variable
inside the brackets next to it.
let synths = [ 'synth-1', 'synth-2', 'synth-3', 'synth-4' ];
console.log(synths.length); //__This is 4
for (let i = 0; i < synths.length; i += 1) {
console.log(synths[i]);
}
The following code shows a modification of the previous code where each value
in the array has “0hz” appended to it.
let synths = ['synth-1', 'synth-2', 'synth-3', 'synth-4']
for (let i = 0; i < synths.length; i += 1) {
synths[i] += "0hz";
}
console.log(synths); //[ 'synth-10hz', 'synth-20hz', 'synth-30hz',
'synth-40hz' ]
While Loops
While loops are useful when you are unsure of how many iterations will be needed
to complete a task. A simple analogy might be one of a live podcast website that
allows users to connect and listen while a show is on the air. As a programmer,
you might not know how long the show will last but you want to continuously
check for new user connections for the duration of the show and allow them to
listen in. The pseudo code for this analogy might look something like this.
let onAir = true;
while(onAir){
// check for new visitors and connect them
}
While loops consist of the while keyword, an opening and closing parenthesis,
and an opening and closing curly brace. A conditional statement is placed in the
parenthesis which allows the loop to iterate as long as the condition remains true.
When the condition becomes false, the loop stops. The following example loops
While Loops 41
as long as the freq variable is greater than zero. At each iteration, the freq
variable decrements until it is zero and the loop terminates.
let freq = 7000;
while (freq > 0) {
console.log(freq);
freq -= 100;
}
Scope
The concept of scope describes how one part of your program can access variables
in another part of your program. To understand scope, you must understand the
concept of a code block. A code block is the space in between the curly braces
containing executable code such as those used with if statements and for loops.
{
}
Variables declared with let, const, and var behave differently depending on the
context they are declared in.
}
console.log(osc,"The assignment in the if statement is a reference
to the globally scoped variable!"); // 500
If a locally declared variable has the same name as a variable in a higher scope,
the locally scoped variable takes precedence when being referenced locally.
let osc = 200; // globally scoped variable
if(true){
let osc = 300; // locally scoped variable
console.log(osc); // references the locally scoped variable and
returns 300
}
When you declare a variable using var, the JavaScript interpreter immediately
(behind the scenes) decouples the declaration from the initialization and moves
the variable declaration to the top of the current scope. The following example
demonstrates this.
The code on the left contains a variable named test, which is declared after
it is initialized.
When this code is run, JavaScript changes the order and places the declara-
tion at the top of the current scope, in effect making the code look identical to the
code on the right. This is why the test variable is not overwritten when the code is
run, even though it appears at first glance that it should be because the variable is
not yet declared. Prior to the addition of let and const to JavaScript, it was always
considered best practice to declare var prefaced variables at the top of the current
scope because that is where they will be declared anyway.
It is now considered good practice to only use let and const for variable dec-
larations and to declare variables at the top of the current scope primarily for
readability.
As mentioned earlier in the book, the let and const keywords were intro-
duced to JavaScript in 2015 as a way to add block scoped variables to JavaScript.
Moving forward, I suggest that you only use let and const to declare your vari-
ables. Using var is considered bad practice and unnecessary. Throughout the rest
of the book, we will not be using var to declare our variables.
Summary
In this chapter, you have learned how to incorporate decision making into your
programs using conditional statements. You have also learned how to use loops
to accomplish tasks quickly and how they can be leveraged when working with
arrays. You learned what block scoped variables are. In the next chapter, you will
learn how to incorporate functions into your programs.
Summary 45
5 Functions
In this chapter, you will learn about functions, various ways to work with func-
tions, and variable scope while using functions. Functions allow you to write
code in a way that avoids repetition. They also allow you to encapsulate your code
and perform a specific task based on a set of inputs. You will learn how to use
variables in functions when writing programs.
DOI: 10.1201/9781003201496-5 47
The following example shows how you might code the effects box example
for a fixed selection. The effectsBox function takes an input, multiplies that
input by two, and returns the result.
function effectsBox(input) {
return input * 2;
}
console.log(effectsBox(120)); // Output 240
The following example shows how you can multiply the input by a value selected
by the user, which is coded in the form of a parameter called multiplier.
function effectsBox(input, multiplier) {
return input * multiplier;
}
console.log(effectsBox(120, 2)); // Output 240
Parts of a Function
To create a function, you start by typing the function keyword followed by
a function name. Immediately following the function name, you place opening
and closing parentheses, and then immediately after these you place opening and
closing curly braces.
function add(){
// function body
}
You can give the function placeholders for input values called parameters, which
you place inside the parentheses and separate by commas.
function add(a, b){
}
48 5. Functions
Commenting parameter inputs is important to ensure that you and other devel-
opers know the intended inputs to a function. A common syntax style to describe
argument type and its intended use is demonstrated below.
/*
@param {string} oscType - an oscillator type such as
square,sine,sawtooth or triangle
@param {number} oscValue - an oscillator value as a number.
*/
function add(oscType, oscValue){
}
The final part of a function is an optional return statement that outputs a value
when the function completes.
function add(a, b){
return a + b;
}
To run the function (also called invoking the function), you type the function
name followed by an opening parenthesis. If the function has parameters, you
enter values for these, which in the context of invoking the function are called
arguments. You end the function with a closing parenthesis.
add(2, 5); // 7
If you invoke the function with arguments not defined by the function, no error
is returned and the system ignores the additional arguments.
add(2, 5, 999) //__The third argument is ignored and output is 7
Function Expressions
As an alternative to using function declaration syntax, you can write your func-
tions using expression syntax, where you assign the function to a variable like
this:
let add = function (a,b){
return a + b;
};
add(2,3); // 5
Function Expressions 49
Abstracting Oscillator Playback
The following function playOsc plays an oscillator and has two arguments.
The first, oscType, determines the oscillator waveform type, which for the Web
Audio API supports sine, sawtooth, triangle, and square in the form of a string.
The second argument is the frequency value in hertz. Because the code necessary
to generate the oscillator is encapsulated in a function, you can now invoke the
function by writing only one line of code each time you create an oscillator. This
means you avoid the repetition of writing out all of the oscillator creation code
every time you create the oscillator.
const audioContext = new AudioContext(); //___Initializes web audio
api
playOsc("sine", 340);
function customSound(filterVal) {
let osc_1 = audioContext.createOscillator();
let filter = audioContext.createBiquadFilter();
filter.type = "lowpass";
osc_1.type = "sawtooth";
osc_1.frequency.value = 300;
filter.frequency.value = filterVal || filter.frequency.value;
osc_1.connect(filter);
50 5. Functions
Default Arguments
JavaScript functions allow for default arguments by pre-assigning them as func-
tions parameters. The following function has two default arguments. The first
default argument is “sine” and the second is “100”.
function makeSound(oscType = "sine",oscVal = 100){
console.log(oscType); // sine
console.log(oscVal); // 100
}
makeSound(); // sine 100
The arguments object can be combined with the length property and a condi-
tional statement to ensure that an error is given if any arguments are left empty.
To create your own error statement, you use the throw keyword. In the follow-
ing code, the conditional statement checks to see if the number of arguments is
not two, and if the conditional statement evaluates to true, then an error is given
(or thrown) to indicate this result.
function playOsc(oscType,freq){
You can add another check to ensure that the correct data types are being entered
like this:
function playOsc(oscType, freq) {
if (arguments.length !== 2) {
throw "Error! This function takes two arguments";
}
//_____Check for correct argument data types
if (typeof oscType !== "string" || typeof freq !== "number") {
throw "Please enter the correct argument types";
}
}
playOsc(100, true); //___Please enter the correct argument types
Rest Parameter
The rest parameter lets you create functions that have an undefined number of
parameters. The rest parameter also gives you access all arguments of a function
as an array. Unlike the arguments object, the rest parameter returns a real array
that responds to JavaScript’s built-in array methods. Using comments to document
the intended inputs of a function is more important when using the rest parameter
because unlike a conventional function, the parameters are not named individually.
function doThing(...allArgs){
console.log(allArgs)
}
doThing("one","two","three"); // [ 'one', 'two', 'three' ]
Function Scope
Scope defines how one part of a program can access variables in another part
of a program. JavaScript’s original design had two forms of scope and these are
global scope and function scope. With the introduction of block scoping, scope
is extended to parts of the language other than functions such as loops and con-
ditional statements. Functions are still the primary building block in JavaScript
used to encapsulate different parts of your program. This means that if you
declare a variable within a function, it is specific to that function and does not
conflict with any other variables that have the same name and are defined outside
of that function. Functions have access to their own variables and they also have
access to any variables in a higher scope, which includes the global scope.
In one of our previous examples, we created a function to play an oscilla-
tor. Notice that although the audioContext variable is not included inside
the playOsc function, it is still accessible. This is because audioContext is
defined in a higher scope: the global scope.
52 5. Functions
//____audioContext is global
const audioContext = new AudioContext();
//____ playOsc has access to it
function playOsc(oscType, freq){
let osc = audioContext.createOscillator();
osc.type = oscType;
osc.frequency.value = freq; //____freq is a parameter
osc.connect(audioContext.destination);
osc.start(audioContext.currentTime);
}
playOsc("sine", 330);//____Plays oscillator at 330hz
You can use function scope to protect variables defined in a function not only
from variables defined in a higher scope but also from variables defined in other
functions. In the following example, there are two functions. One has data, and
the other wants data. The data of the first function is not accessible to the other
function because it is hidden in a local scope.
function iHaveData() {
let data = "The data"; // <-- this variable......
}
function iWantData() {
return data; // <-- is not accessible here...
}
iWantData(); // data is not defined
If you declare two variables with the same name and one is globally scoped (or
in a higher scope) and the other is locally scoped within a function, the locally
scoped variable is referenced when the code in the function is running.
So, for example, in the following code, the multFreq function takes a single
argument and multiplies it by a value that is assigned to the multiplier vari-
able. The globally scoped multiplier is not referenced when multFreq() is
running because the function has a locally scoped variable with the same name.
let multiplier = 4; //______This variable is not referenced by
multFreq
function multFreq(frequency) {
let multiplier = 2; //_____because this one has the same name and
is locally scoped!
return frequency * multiplier;
}
console.log(multFreq(200)); // 400
console.log(multiplier); // 4
If the locally scoped multiplier variable declaration inside the previous func-
tion is removed, then during function execution, the code will look outside the
function for a variable with the referenced name.
let multiplier = 4;
function multFreq(frequency) {
/*__There is no local multiplier variable so it finds one in the
scope above it__*/
return frequency * multiplier;
Function Scope 53
}
console.log(multFreq(200)); // 800
54 5. Functions
The following example of the same function written using expression syntax,
however, throws an error. This happens because function expressions are treated
like variables, with the declaration being hoisted to the top. Remember that the
initialization of the variable still happens where the variable is initialized in the
code. In this case, the function is run before the initialization that defines the
function occurs. The lesson here is that when you use function expressions, you
must declare functions before you invoke them. This is good practice with all
your functions as it makes your code less confusing and more readable.
multFreq(200, 2); // ___ error! “multFreq is not a function”.
let multFreq = function(input, val) {
return input * val
}
let multFreq = function(input, val) { // declaration before
invocation!
return input * val
}
multFreq(200, 2); // works as expected
Anonymous Functions
Anonymous functions are functions that do not have a name. Technically, the
function in the following code is an anonymous function because the variable
it is assigned to is not the function name. It is the container name for an anony-
mous function.
let multFreq = function(input, val) {
return input * val
}
To give this function a name, you do it like this:
Note, however, that to invoke the function, you use the variable name that it is
assigned to.
multFreq(100,2) // 200
Anonymous Functions 55
To view the output, you can wrap it in console.log().
console.log(
//______________________BEGIN IIFE
(function run() {
return "data";
}())
//______________________END IIFE
);
The first thing to notice is that the function is wrapped in parentheses. This is
optional, but is considered best practice because it helps differentiate the con-
struct syntactically from non-IIFE functions.
(function run() {
return "data";
}());
The next thing to notice is the parentheses toward the end of the function before
the closing, encapsulating parenthesis. This syntax is what invokes the function.
(function run() {
return "data";
}());
To add parameters and arguments, you put parameters in the first set of paren-
theses and arguments in the second set of parentheses.
(function add(a, b) { //_____ parameters
return a + b;
}(2, 3)); //___________________arguments
Closures
One of the most difficult aspects of the JavaScript language for new programmers
to grasp is closures. Understanding closures will ultimately allow you to write
cleaner code while giving you a powerful tool to solve a host of problems you will
inevitably run into. Understanding the concept of closure can be a bit difficult at
first. But in the long term, the benefits are worth the time investment.
What Is a Closure?
A closure is an inner function that has access to the scope of its outer environ-
ment even after that outer environment has returned. To understand what this
means, you must first solidify your understanding of scope. The following exam-
ple demonstrates how a function has access to its local scope, the global scope,
and its local arguments.
let globalVariable = "global variable";
function doSomething(argInput) {
56 5. Functions
As we mentioned, a closure is an inner function that has access to the scope of its
outer environment even after that outer environment has returned. The previous
examples demonstrated scope access. The following example demonstrates what
it means for a function to have scope access even after the outer environment has
returned. The outer environment can be either the global environment or another
function. The following code includes the effectsBox function that contains a
single variable named component. The effectsBox function returns a func-
tion that returns the value of component. When the initial effectsBox func-
tion is invoked, it returns a function declaration named openEffectsBox to
the outer scope (in this case the global scope). This openEffectsBox function
What Is a Closure? 57
declaration is then assigned to a variable called getComponent, which is then
invoked and returns the string “Pulled out component”.
The important thing to realize here is that a closure (the inner function) can
return data (such as the component variable) from its containing environment
[in this case effectsBox()] even after that outer environment [effects-
Box()] has returned.
function effectsBox() {
let component = "Pulled out component";
return function openEffectsBox() {
return component;
};
}
let getComponent = effectsBox(); //___stores "openEffectsBox"
function in a variable.
console.log(getComponent()); // "Pulled out component"
The previous example can be modified to demonstrate how state can be modi-
fied and retained using the closure. In this code, there is an additional coun-
ter variable that increments each time the inner openEffectsBox function
is invoked. Since closures allow access to the scope of a containing function even
after that containing function has returned, the returned function can continue
to increment the counter variable and have access to its state.
function effectsBox() {
let counter = 0;
let component = "Pulled out component";
return function openEffectsBox() {
return component + " " + (counter += 1);
};
}
let getComponent = effectsBox(); //___stores "openEffectsBox"
function in a variable.
function playOsc(type) {
return function(freq) {
58 5. Functions
Callback Functions
A callback is a function that is used as an argument to another function. The
following example demonstrates addition of two numbers using a callback.
function doMath(callback) {
return callback();
}
function addTwoNumbers() {
return 2 + 2;
}
doMath(addTwoNumbers); // 4
When working with callbacks, you will often see function invocations where the
callback declaration is placed directly in a function argument.
function doStuff(callback) {
return callback();
}
Callback Functions 59
}
}
function diff(a, b) {
return Math.abs(a - b);
}
console.log(calculateFrequencies(200, 2));// 400___Multiplies
numbers
console.log(calculateFrequencies(1000, 4000, diff));// 3000___uses
custom callback to find the difference
The previous example demonstrates how passing a callback to a function provides the
action taken by the callback, whereas passing nonfunction values provides data input.
filter()
The filter method compares each element in an array to a conditional state-
ment and returns a new array of only those elements that meet the filter condition.
The following example uses filter() to loop through an array of frequency
values to create a new array of values greater than or equal to 1000.
let freq1 = 1200,
freq2 = 570,
freq3 = 100,
freq4 = 1500;
60 5. Functions
map()
The map function calls a function on each element in an array and returns a new
array that contains the mapped data for each element in the input array.
The following example uses map() to add 100 to each value in an array and
return a new array named newFreqs.
let freqs = [100, 200, 300];
let newFreqs = freqs.map(function (val) {
return val + 100;
});
console.log(newFreqs); //__ [ 200, 300, 400 ]
The callback functions of both map() and filter() take three arguments. In
order of their position, these are value, index, and array. The value argu-
ment is the array value at the current index, the index argument is the cur-
rent index value, and the array argument is the array that the callback is being
applied to. In the following example, a map method is applied to an array and all
three arguments are logged to the console.
let freqs = [100, 200, 300];
let newFreqs = freqs.map(function(val, index, arr) {
let message = "current value: " + val + " current index index:
" + index + " array: " + arr;
console.log(message);
return val;
});
/*___This logs the following to the console
current value: 100 current index: 0 array: 100,200,300
current value: 200 current index: 1 array: 100,200,300
current value: 300 current index: 2 array: 100,200,300
*/
Arrow syntax can be made more concise if the callback only has a single argu-
ment. In such a case the parenthesis can be removed.
let freqs = [100, 200, 300];
let newFreqs = freqs.map(val => { // argument parenthesis removed
return val + 100;
});
If the callback function is only used to return a value and does not contain code
in the body, the return statement and curly braces can be removed as well.
let freqs = [100, 200, 300];
let newFreqs = freqs.map(val => val + 100); // argument
parenthesis, return statement and curly braces removed.
Typically, if a function expression does not contain a body of code and is only
used to return a value, it is written like the following example.
let add = (valOne,valTwo) => valOne + valTwo;
console.log(add(2,3)); // 5
62 5. Functions
Recursion
Recursion is an advanced programming topic, and it will only be explored briefly
in this chapter.
A recursive function is a function that calls itself. The following is an exam-
ple of a recursive function.
function x(){
return x()
}
If you run the previous code, it will crash your browser. This is because, when a
recursive function runs indefinitely, it eventually uses up the resources of your
code interpreter (in this case the web browser) and creates an error. To use recur-
sion effectively, you need to set a condition to terminate the recursion. This con-
dition is called the base case.
The following example is a recursive function named loopFromTo that
contains a working base case. loopFromTo takes two arguments, and both
are numbers. In the function body, a conditional statement is used to check if
the argument named start is less than the argument named end. As long as
this condition is true, loopFromTo calls itself and on each iteration incre-
ments the start argument by one. This continues until the recursion termi-
nates when start ceases to be less than end and the conditional statement
evaluates to false.
function loopFromTo(start, end) {
console.log(start);
if (start < end) {
return loopFromTo(start += 1, end)
}
}
Recursive functions can be used in place of looping constructs and are an invalu-
able tool in many complex algorithms. If recursion seems confusing don’t worry,
you can program perfectly good applications while you become familiar with it.
Summary
In this chapter, you learned how to create and use functions. In the next chapter,
you will expand your understanding of JavaScript to include a concept called
object-oriented programming.
Summary 63
6 Objects
So far, we discussed six of JavaScript’s eight data types. These are string, num-
ber, Boolean, undefined, BigInt, and null. These are called primitive data types.
Anything that is not a primitive data type is of the object data type. In the previ-
ous chapter, you learned about functions, which are of the object data type. In
this chapter, you will learn how to program using object literals, which are also
of the object data type.
◾ String
◾ Number
◾ Boolean
◾ Undefined
◾ Null
DOI: 10.1201/9781003201496-6 65
◾ Object
◾ BigInt
◾ Symbol
The object data type includes functions, arrays, and object literals. Arrays and
functions have already been explored, so here is a general definition of object
literals: Object literals are a collection of comma-separated key-value pairs that
are contained within curly braces.
In the following code, an object named obj is created and the values within
curly braces are assigned to it.
let obj = {
key1: "value1",
key2: "value2"
};
A key is similar to a variable, and a value is similar to the data assigned to a vari-
able. The key and value of an object is called a property for nonfunctions assigned
to a key, or a method for functions assigned to a key.
let obj = {
key: "value", //___This is a property
doSomething: function(){ //___This is a method
}
};
Another syntax for creating methods is demonstrated below. The syntax works
identically to the code above.
let obj = {
key: "value", //___This is a property
doSomething(){ //___This is a method with a more concise working
syntax
}
};
Conceptually, object literals are used to model real-world elements in your code.
So, for example, the following object is used to model a music album.
//_________________This is an object that contains album
datalet album = {
66 6. Objects
name:"Thriller Funk",
artist:"James Jackson",
format:"wave",
sampleRate:44100
}
To access data from an object, you can use dot notation, which looks like this:
album.name; // Thriller Funk
album.artist; // James Jackson
album.format; // wave
album.sampleRate; // 44100
If you use a bracket notation, you must type the key in the form of a string.
album[
"sampleRate"
];
You can invoke methods with dot notation and trailing parentheses.
//__________________________________BEGIN method invocation
song.nameAndArtist(); // Name: Funky Shuffle| Artist: James Jackson
//__________________________________END method invocation
The structure of a for in loop consists of the for keyword followed by a variable
that represents the value of each property. In the previous example, this variable
was named prop. The variable name is followed by the in keyword and the name
of the object you want to loop through.
Often you will want to modify the properties of an object you are looping
through while not modifying any of its methods. One way you can do this is by
using a conditional statement and the typeof operator to act only on property
values that are not functions. This usage is shown in the following code:
let song = {
name: "Funky Shuffle",
artist: "James Jackson",
format: "wave",
sampleRate: 44100,
nameAndArtist() {
return "Name: " + song.name + " | " + "Artist: " + song.artist;
}
};
for (let prop in song) {
if (typeof song[prop] !== "function") {
console.log(song[prop]); //___Omits methods
}
}
68 6. Objects
name: "Funky Shuffle",
artist: "James Jackson",
getArtist() {
return song.artist;
}
};
console.log("artist" in song); //true
console.log("getArtist" in song); //true
Cloning Objects
If you want to create an object that has access to another object’s properties and
methods, while being extensible, you can use the Object.create() function.
The following example shows an object being cloned using this method.
let effect = {
type:"reverb",
value:"50%"
};
You can then extend the newly created object with properties and methods.
newEffect.fuzz = "20%";
newEffect.drive = "10%";
Cloning Objects 69
Prototypal Inheritance
It is important to understand that Object.create() does not literally copy the
properties and methods to a new object but provides a reference to the properties
and methods contained in the parent object(s). This hierarchy of references between
objects is called prototypal inheritance. The following code shows this by cloning
multiple objects and including comments of the hierarchy of property accessibility.
let synth = {
name: "Moog",
polyphony: 32
};
let synthWithFilters = Object.create(synth); //clone synth
/*The original synth object does not have access to the filters
property. */
let synthWithFiltersAndEffects = Object.create(synthWithFilters);
//clone synthWithFilters
name: "Moog",
polyphony: 32,
};
70 6. Objects
This behavior is duplicated with arrays because arrays are of the object data type.
let synth = {
name: "Moog",
polyphony: 32,
filters: ["lowpass", "highpass", "bandpass"]
}
let newSynth = Object.create(synth);
newSynth.name = "Roland";
console.log(synth.name) // this does not change and remains "Moog".
// however, watch this!
newSynth.filters[0] = "comb";
// The original is changed!
console.log(synth.filters) // [ 'comb', 'highpass', 'bandpass']
72 6. Objects
//_______Then invoke it!
console.log(getNameOfSong()); // Funky Shuffle
If you want to specify arguments in a function created with bind, you can do
this in one of two ways. The first is to specify the arguments in the newly created
function. In the following example, a function named descriptor is invoked on
an object named blastSound. An argument is then passed to the describe-
BlastSound function.
let blastSound = {
name: "Blast"
};
function descriptor(message) {
return this.name + ": " + message;
}
let describeBlastSound = descriptor.bind(blastSound);
console.log(describeBlastSound("This is an explosive sound"));
//Blast: This is an explosive sound
Alternatively, you can specify the arguments in the statement where you bind the
function to the object. You do this by first specifying the object to bind to, then
specifying arguments you want to use and separating them with commas, as in
the following example:
let describeBlastSound = descriptor.bind(blastSound, "This is an
explosive sound");
console.log(describeBlastSound()); /*Blast: This is an explosive
sound*/
As you can see, even when a function has not been written as a method on a par-
ticular object, you can still apply the function to that object. This also means that
you can use a method of one object and apply it to a completely different object.
The following code uses a method named getNameAndArtist of an object
named song and applies it to an object named album.
let album = {
name: "Funky Shuffle",
artist: "James Jackson",
format: "wave",
sampleRate: 44100
};
let song = {
name: "Analogue Heaven",
artist: "The Keep It Reels",
getNameAndArtist() {
return "Name: " + this.name + " | Artist: " + this.artist; }
};
let getNameOfAlbum = song.getNameAndArtist.bind(album);
console.log(getNameOfAlbum()); /*Name: Funky Shuffle | Artist:
James Jackson*/
If a function is invoked outside the context of an object, its this value points to
one of two values, depending on whether strict mode is used or not. If strict mode
Summary
In this chapter, you learned how to program with objects. In the next chapter,
you will learn the basics of the Web Audio API node graph and working with
oscillators.
74 6. Objects
7 Node
Graphs and
Oscillators
In previous chapters, you learned the basics of working with JavaScript data
types and how to use the Web Audio API to generate basic tones. In this chapter,
you will use your understanding of JavaScript to get a better understanding of
two core features of the Web Audio API: node graphs and oscillators.
AudioContext() is a constructor that returns an object when you use the key-
word new. Constructors and the new keyword are explained in Chapter 12. For
now, the important thing to understand is that AudioContext() returns an
object containing all of the methods and properties that you use to access the
Web Audio API.
DOI: 10.1201/9781003201496-7 75
Node Graphs
A node graph is a collection of nodes. A node in a node graph is an object that
represents an audio input source, such as an oscillator, or an object designed to
manipulate an audio input source such as a filter. These nodes are connected
together using a method named connect.
The following code is an example of an oscillator node connected to a filter
node.
"use strict";
const audioContext = new AudioContext();
//_____________BEGIN create oscillator and filter
let filter = audioContext.createBiquadFilter();
let oscillator = audioContext.createOscillator();
//_____________END create oscillator and filter
//______________BEGIN connect oscillator to filter
oscillator.connect(filter);
//_____________END connect oscillator to filter
//_____________BEGIN connect filter to computer speakers
filter.connect(audioContext.destination);
//_____________END connect filter to computer speakers
//_____________BEGIN start oscillator playing
oscillator.start(audioContext.currentTime);
//_____________END start oscillator playing
In the previous code, the oscillator object is created using the createOs-
cillator method of the audio context and stored in a variable named
oscillator. You create the filter object in a similar way by invoking the
createBiquadFilter method of audioContext. The oscillator is
connected to the filter using connect(). The filter is connected to a prop-
erty named destination. The destination represents the output of your
computer’s audio system. To start the oscillator playing, you use a method of the
oscillator object named start. The start method takes one argument
that determines the time the oscillator starts playing. The value of audioCon-
text.currentTime is the current time in seconds within the Web Audio
API, starting when AudioContext was invoked. (The topic of time is dis-
cussed in Chapter 21.)
Oscillators
Oscillators, like all Web Audio API nodes, have their own custom proper-
ties and methods. The following methods and properties are discussed in this
chapter:
Method Description
start Starts oscillator playing
stop Stops oscillator playing
◾ sawtooth
◾ sine
◾ square
◾ triangle
Summary
In this chapter, you learned the basics of node graphs and oscillators. In the next
chapter, you will learn the basics of HTML and CSS and create the interface for
your first Web Audio API applications.
In this chapter, you will learn the basics of HTML and CSS, giving you the neces-
sary tools to build user interfaces for your Web Audio API applications. You will
do this by building a user interface intended to trigger an oscillator that includes
interactive controls to select frequency and waveform type. In the next chapter,
you will combine the interface with JavaScript code to build your first working
interactive application.
HTML
HTML stands for hypertext markup language and is the language used to create
static websites. In Chapter 1, you learned that HTML consists of elements, some-
times referred to as tags, that make up the page of an HTML document. To be
DOI: 10.1201/9781003201496-8 81
treated as an HTML document, a file must be saved with .html appended to its
name. A file extension is a group of characters placed after a period in a file name
that indicates the file’s format. In the case of a file named index.html, the file
extension is .html.
The following code is from the HTML template you created in Chapter 1. It
consists of a collection of elements required to make a document W3C compliant.
W3C stands for World Wide Web Consortium; this group is responsible for the
development of web standards. Unlike JavaScript, HTML does not return errors if
your code is written incorrectly, so you need additional tools to find HTML errors.
You can test the compliance of an HTML document by running your code through
the HTML validation tool at the following URL: https://validator.w3.org.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>app</title>
<script src="js/app.js"></script>
<link rel="stylesheet" href="css/app.css">
</head>
<!--_____________________________________________ BEGIN APP-->
</body>
<!--_____________________________________________ END APP-->
</html>
Inside the <body> element is where you write the bulk of your HTML code. In
the following example, the <p> and <h1> elements between the opening and
Second, most elements contain opening and closing tags that are used to encap-
sulate other elements. The processes of encapsulating elements within other ele-
ments and treating the containing elements as boxes are commonly referred to
as the box model.
The following code emphasizes the box model by adding elements that
contain other elements. This includes a containing <div> that encapsulates a
<form> element. The <form> element then encapsulates <input>, <span>,
and (paragraph) elements.
Creating an Interface </h1>
In this chapter we will go over HTML and CSS</p>
<div>
<form>
<input id ="on-off" type = "button" value="start">
<span>Click to start oscillator</span>
Use slider to modify frequency</p>
<input type= "range">
</form>
</div>
</body>
The type attribute comes with a built-in list of possible settings, some of which
are shown in the following code. The value attribute gives the <input> ele-
ment a default setting, as shown in the following demonstration code (code that
is not used in your final application):
<form>
Input element type set to "button"</p>
<input type = "button" value="start">
<hr>
Input element type set to "range"</p>
<input type= "range">
<hr>
Input element set to a "number"</p>
<input type = "number" value="44.100">
<hr>
Input element set to a "text"</p>
<input type= "text" value ="sine">
</form>
</body>
CSS
CSS stands for cascading style sheets and is the technology used to style web
pages and web applications. Like HTML, CSS does not throw errors when writ-
ten improperly. To check for errors, you can use the W3C CSS validator tool at
this URL: https://jigsaw.w3.org/css-validator/.
CSS 87
CSS files use the .css file extension. To use CSS with an HTML file, you
must first create a CSS document and then connect it to your HTML document
using the <link> element in the <head>. The following example illustrates this
usage, which is applied for the remainder of this chapter.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CSS and HTML</title>
<link rel="stylesheet" href="css/app.css">
</head>
To ensure that your CSS document is being read properly, open your HTML doc-
ument in Chrome and open the developer tools. If you made an error, the console
will indicate this in red.
After the CSS file is linked you can begin to apply CSS styling to the HTML
elements. For example, if you want to change the background color of the page,
in your CSS file you select the body element and set the background-color
property to a color value. This is shown in the following example where the back-
ground color is changed to orange. As an alternative to using the name of the
color, you can set the color using a hex color code value such as #ffa500 or a
red–green–blue value such as rgb(255,165,0).
body{
background-color:orange;
}
1. Select the element you want to affect and type its name in your CSS file.
In the previous example, this was body.
2. Type an opening and closing curly brace. These two braces are commonly
referred to as a code block. Inside the code block, you place properties
and set values following a colon. In the previous example, the property
was background-color and its value was orange. Each property
value setting ends with a semicolon.
The CSS specification includes many properties. A full list of properties is avail-
able at this URL: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference.
CSS 89
Comments
Just like HTML or JavaScript, you can add comments to your CSS file using the
following syntax:
/* This is a CSS comment */
Element Selectors
When you select elements directly, all instances of the element are selected and
the same CSS styling is applied to them. For example, in the following demon-
stration code, every <div> on the page is selected and given a background color
of blue.
div{
background-color:blue;
}
Grouping Selectors
If you want to apply the same styles to multiple selectors, you can do so in one
line of code by grouping selectors. You do this by separating each element with a
comma. The following demonstration code selects the, p, <li>, and h1 elements
and applies the same font color to each one.
p,li,h1{
color:green; //Changes font color of all three elements to green
}
Descendent Selectors
If you want to access an element only if it is nested inside of a particular element,
you can do this with descendent selectors. The CSS syntax for this type of selector
is expressed by typing the parent element, a space, and then the element you want
to select. In the following code, a descendant selector is used to select all <li>
elements that are nested in any <div> element. In the following demonstration
code, the font color of each <li> element is set to blue.
div li{
color:blue;
}
It is important to realize that descendant selectors select all the descendent ele-
ments no matter how nested they are. If the previous CSS example were applied
to the following HTML code, it would change the font color of all of the <li>
elements to blue even though they are nested in a <ul> element.
Child Selectors
Child selectors are similar to descendent selectors with the difference that the
selected element can only be one level deep relative to the parent. A child selec-
tor is made using the “>” symbol with the parent element on its left side and the
child element on its right. The following demonstration code will select all <li>
elements that are children of <ul> elements.
ul > li{
/* do something */
}
class and id
Often when selecting elements, you do not want to select every element of a par-
ticular type. Rather, you might want to select either individual instances of ele-
ments or groups of elements. You can single out an individual element for styling
by using an identifier called id. Conversely, you can designate a collection of
elements as a group by using an identifier called class.
To single out an element using an id, you must first define id as an attribute
of an HTML element. The syntax looks like the following:
<div id = "controls">
<!—- content -->
</div>
In your CSS, you can then select this individual element by preceding its id with
a hashtag character.
#controls{
/* properties and values go here */
}
Keep in mind that id names are intended to be used only once in your HTML
and are applied to a single element!
Now that you know how to single out an element using id selectors, it is time to
learn how to group elements using class selectors.
In your HTML document, assign the two <div> elements the class osc-
controls. Then encapsulate the first and inside a new <div>, and place
another <div> element at the bottom of the page that contains a paragraph ele-
ment with the phrase “JavaScript for Sound Artists Demo” inside of it.
You should have a total of four <div> elements in this code, which looks
like this:
<body>
<div>
Creating an Interface </h1>
In this chapter we will go over HTML and CSS</p>
</div>
<div class="osc-controls">
<form>
<input id ="on-off" type = "button" value="start">
To select the osc-controls class from CSS, you must preface the class name
with a dot.
.osc-controls{ /* Notice the dot selector */
/* set property values */
}
In the following code, the osc-controls class is given a border. Only the mid-
dle two <div> elements respond to these changes because the first and third
<div> on the page do not have the class osc-controls assigned to them.
.osc-controls {
border-style:solid;
border-color: #BC6527;
border-width: 2px;
border-radius:10px;
}
To create a bit of space between the text and a <div> element border, include the
following code in your CSS file:
div{
padding:20px;
}
The two outlines around the middle <div> elements could use some space
between them. The following code creates this space by using the bottom-
margin property with a value of 20px.
.osc-controls {
border-style:solid;
border-color: #BC6527;
border-width: 2px;
border-radius:10px;
margin-bottom:20px;
}
You can remove the space previously occupied by the bullet points by setting
the padding-left property to zero on the parent <ul> element.
#oscillator-list{
padding-left:0px;
}
The default font type for Chrome is Times New Roman. You can change the font
type if you like. The following code changes the font type to Arial.
body{
background-color:orange;
font-family: "Arial";
}
.application {
width: 550px;
margin: 0 auto;
}
p,
span,
li,
input {
font-size: 1.5em;
}
#sawtooth {
background-color: #336E91;
}
#sine {
background-color: #783d47;
}
#triangle {
background-color: #3b3040;
}
#square {
background-color: #b85635;
}
.osc-controls {
border-style: solid;
border-color: #BC6527;
border-width: 2px;
border-radius: 10px;
margin-bottom: 20px;
}
div {
padding: 20px;
}
#oscillator-list li {
/* Descendent selector */
list-style-type: none;
color: white;
}
#oscillator-list {
padding-left: 0px;
}
This chapter shows you how to add JavaScript to CSS and HTML. By the end of the
chapter, you will have created a fully functioning application with interactive controls.
HTML
<body>
<input id="play-button" type="button" value="PLAY">
</body>
The next line of code applies the eventListener method to the playButton.
playButton.addEventListener("click", function() {
alert("You clicked the play button");
});
In the app.js file, make sure you have strict mode enabled. All JavaScript code
is written below the use strict string.
"use strict";
Although this code starts the oscillator playing, it does not stop it from play-
ing. The following changes implement the stop feature by adding a conditional
statement to addEventListener to check whether a variable named osc is
set to false. If osc is false, an oscillator is created and assigned to it. This makes
the Boolean value of the osc variable true, and the start method is invoked,
allowing the oscillator to play. If the user clicks the Start button again, the condi-
tional statement sees that the osc variable has the Boolean value true and runs
the code in the else branch. This stops the oscillator from playing and resets
osc to false.
"use strict";
const audioContext = new AudioContext();
window.onload = function() {
let onOff = document.getElementById("on-off");
/*_________________________________________BEGIN set initial
osc state to false*/
let osc = false;
/*_________________________________________END set initial
osc state to false*/
onOff.addEventListener("click", function() {
/*_____________________________________BEGIN Conditional
statement to check if osc is TRUE or FALSE*/
if (!osc) { /*_________________________Is osc false? If so then
create and assign oscillator to osc and play it.*/
osc = audioContext.createOscillator();
osc.type = "sawtooth";
osc.frequency.value = 300;
osc.connect(audioContext.destination);
osc.start(audioContext.currentTime);
/*_________________________________Otherwise stop it and
reset osc to false for next time.*/
} else {
osc.stop(audioContext.currentTime);
osc = false;
}
/*_____________________________________END Conditional
statement to check if osc is TRUE or FALSE*/
});
};
It is important to understand that DOM elements are not arrays, even though the
notation used to select them is similar to that used for arrays. DOM elements are
referred to as nodes.
Next, create a variable named waveformTypes and assign it the result of call-
ing getElementsByTagName(). The waveformTypes value is used to
select one of the four <li> elements on the page.
let waveformTypes = document.getElementsByTagName('li');
Next, create a function named select that is used as a callback for a series of
event listeners used to select the id of the <li> clicked by the user.
function select() {
selectedWaveform = document.getElementById(this.id).id;
console.log(selectedWaveform); // Outputs id
}
In the CSS file, create a class named selected-waveform and give it an out-
line property with a width of 2 pixels and the color white. Then, add this class
dynamically to the <li> element that corresponds to the selected waveform
type. To remove the selected waveform class of the previously selected element,
use a for loop to examine all of the <li> elements and invoke classList.
remove("selected-waveform") on each one.
In the CSS file, create the class.
.selected-waveform{
outline:2px solid white;
}
Summary
In this chapter, you learned how JavaScript interacts with the DOM. In the next
chapter, you will learn the basics of a library named JQuery that makes DOM
programming with JavaScript easier.
In the previous chapter, you learned how JavaScript interacts with the DOM. In
this chapter, you will learn how to simplify the process of adding interactive com-
ponents to your application by using a library called JQuery. The objective of this
chapter is not to teach you the entire JQuery API, but to give you the foundational
knowledge to make JQuery a part of your programming toolkit. You can find the
JQuery API at this URL: https://api.jquery.com/.
What is JQuery?
JQuery is a library written in JavaScript intended primarily for DOM manipula-
tion. A library is a collection of preassembled code pieces designed to make a par-
ticular group of tasks easier. JQuery contains a large collection of methods and
properties that can be used individually or combined to help ease the complexity
of JavaScript DOM programming.
JQuery Setup
You can set up JQuery in one of two ways. The first is to download the library and
reference it from an HTML file. The second is to reference it from a content deliv-
ery network (CDN). A CDN is a service accessible through the World Wide Web
In the previous chapter, your JavaScript code was encapsulated in the following
function:
window.onload = function() {
// code goes here
};
The syntax for element selectors always begins with a dollar sign, followed by two
parentheses. You place the element wrapped in quotes inside the parentheses.
JQuery selectors borrow from CSS selector syntax. If you know how to select ele-
ments using CSS, you can quickly learn to select elements in JQuery. The follow-
ing are a few examples of CSS selectors and their JQuery counterparts:
Method Summary
on Attaches event listeners to an element
css Modifies the CSS of an element
fadeIn Fades in an element over time
fadeOut Fades out an element over time
val Sets or gets the value attribute of an input element
addClass Adds a class to an element
removeClass Removes a class from an element
eq Selects an element based on an index value
text Sets or gets the text of an element
HTML
<div>Play</div>
<div>Stop</div>
<div>Rewind</div>
<div>Fast Forward</div>
<div>Pause</div>
HTML
<div>Play</div>
<div>Stop</div>
<div>Rewind</div>
<div>Fast Forward</div>
<div>Pause</div>
CSS
div{
display:none;
}
JQuery/JavaScript
$(function() {
$("div").css({
backgroundColor: "orange",
width: "100px",
borderStyle: "solid"
}).fadeIn(1000).fadeOut(1000); // example of method chaining
});
The following HTML code contains an input element with its type attribute
set to button. This is selected with JQuery and set to respond to click events via
an event listener. The method used for this is on(), which takes two arguments.
The first argument is a string that defines the event type and the second is a call-
back that is invoked when the event is fired.
HTML
<input type="button" value = "Play">
HTML 119
JQuery/JavaScript
$(function() {
$("input").on("click",function(){ //click event listener
alert("You clicked play");
});
});
HTML
<input type="button" value = "Play">
<input type="button" value = "Pause">
<input type="button" value = "Stop">
JQuery/JavaScript
$(function() {
$("input").on("click",function(){ /*assign event listener to all
input elements*/
console.log($(this).val()); /*use the this keyword to access the
element clicked and return its value property*/
});
});
In the new version of your code, make sure you replace window.onload with the
equivalent JQuery function. Also put “use strict” and the AudioContext
instantiation at the top of the file, as in the following example:
"use strict";
const audioContext = new AudioContext();
$(function(){
// all code will go here
});
Next, modify the first three variables of the JavaScript file to use JQuery selectors.
Without JQuery
With JQuery
This code uses $(“#on-off”) to select the oscillator start or stop button by id.
It is denoted by the hash selector. You then use $(“span”) to select the span
that contains message text. This selection is done by element and, because there
is only one span element on the page, you do not need to be more specific. Lastly,
$(“input”).eq(1).val() is used to select the range slider value of the second
input element on the page, which is stored in a variable named $freqSliderVal.
This is done by making a general element selection for all input elements and
specifying the second one on the page with the eq(1) method. The eq() method
enables selection of elements by index with its argument being the index value.
Once the correct input element is selected, val() is used to get its value attribute.
The refactored JQuery code assigns a click event listener to all <li> elements.
When the user clicks an <li> element, its id is stored in a variable named
selectedWaveform. selectedWaveform is referenced in a higher scope
and is used later to set the oscillator type. The removeClass() method is used
to remove the selected-waveform class from all <li> elements. The last line
of code uses $(this) to select the specific <li> the user clicked and invokes
addClass() to give it the class selected-waveform.
The remaining changes require you to modify the name of the onOff selec-
tor variable to $onOff and replace the addEventListener with the on()
method set to respond to click events. Then rename the freqSliderVal to
$freqSliderVal, replace the span.innerHTML with $messageText.
text(), and lastly replace onOff.value with the JQuery equivalent of
$onOff.val().
Summary
In this chapter, you learned the basics of using JQuery for DOM manipulation.
You also refactored the code in the previous chapter to contrast the difference
between working with and without JQuery. In the next chapter, you will learn
how to import and play back audio files with the Web Audio API.
Summary 125
11 Loading
and Playing
Audio Files
In this chapter, you will learn the basics of working with audio files. This includes
how to load, play, and run audio files through the node graph to take advantage
of its built-in effects.
Prerequisites
To load and play back audio files, you must be running a web server. Chapter 1
gives you instructions about how to integrate a web server with Sublime Text by
installing a package called Sublime Server. Your audio files are referenced from
the directory the web server is pointing to, which you can set up as follows:
1. Request and store the audio file in a buffer using either the
XMLHttpRequest Object or Fetch API.
The first step is to create a new XMLHttpRequest object. This object allows you
to import data over the http protocol, which is the same protocol used to load
web pages. This data can then be stored in various forms. The code to create the
object is as follows:
let getSound = new XMLHttpRequest();
◾ open
◾ responseType
◾ onload
◾ response
◾ send
The next line of code uses the open method to fetch the audio file from its respec-
tive directory. This method has three arguments.
let getSound = new XMLHttpRequest();
getSound.open("get","sound/snare.mp3",true);
get Requests
When you type a URL into a web browser and “go” to a website, the web browser
does not actually go anywhere. What actually happens is the browser issues a
command to the website server that initiates a download of the HTML content
and other files needed to view it. This command is called a get request. The beauty
of get requests is that you can use them outside the context of typing a URL
into a browser. In other words, you can write code to run get requests behind
the scenes. This is how XMLHttpRequest is used to pull audio files into your
application—and why the first argument of the open method is “get”.
The second argument to the open method is the path to the file you want to
fetch. For this example, an MP3 file named snare.mp3 is imported.
The next line of code begins with an onload function that is invoked after the
data (the audio file) has completed loading. Within the onload function, decod-
ing of the audio data takes place that makes it usable by the Web Audio API. You
do this with a method of the AudioContext called decodeAudioData that
takes two arguments. The first argument is a property called response that
represents the loaded (and undecoded) audio data.
getSound.onload = function() {
audioContext.decodeAudioData(getSound.response,
function(buffer) {
audioBuffer = buffer;
});
};
The last line is the send method. This method initiates the XMLHttpRequest.
getSound.send();
You can now connect the buffer to the audioConext.destination and set
the start time.
function playback() {
let playSound = audioContext.createBufferSource();
playSound.buffer = audioBuffer;
playSound.connect(audioContext.destination);
playSound.start(audioContext.currentTime);
}
The last line of code is an event listener that lets you play back the file when the
window is clicked.
window.addEventListener("mousedown", playback);
If you click on the page, you should hear the audio file play.
fetch("./sounds/snare.mp3")
.then(data => data.arrayBuffer())
.then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
.then(decodeAudio=> {
audioBuffer = decodeAudio
})
function playback(){
const playSound = audioContext.createBufferSource();
playSound.buffer = audioBuffer;
playSound.connect(audioContext.destination);
playSound.start(audioContext.currentTime);
}
window.addEventListener("mousedown",playback);
To use the Fetch API we use the function called fetch. By default, this func-
tion invokes a get request to the specified endpoint. In this case, the endpoint is
“./sounds/snare.mp3”.
fetch("./sounds/snare.mp3")
The first callback makes the data available as an array buffer to our program.
The second callback takes the array buffer and decodes it making it usable to the
Web Audio API. The third callback assigns the decoded audio data to a variable
in a higher scope. Each of these actions is performed one after another.
The following code connects the newly created buffer to the audio graph for
playback.
function playback(){
const playSound = audioContext.createBufferSource();
playSound.buffer = audioBuffer;
playSound.connect(audioContext.destination);
playSound.start(audioContext.currentTime);
}
window.addEventListener("mousedown",playback);
Summary
Each time you want to import an audio file into your program, you must either
initiate XMLHttpRequest with all the method and property settings shown in
this chapter or use the Fetch API. Although the process is simplified using the
Fetch API, you can imagine that duplicating this code repeatedly for each file
is unfeasible for a large-scale application. By abstracting away this complexity,
you can program a solution to this problem that lets you import multiple audio
files with only a few lines of code. In the next two chapters, you will learn how to
do this while learning about three new object creation methodologies: factories,
constructors, and classes.
In the previous chapter, you learned how to import audio files. You also learned
that loading multiple files can require a tremendous amount of code duplica-
tion. Because repeating code is something that should be avoided, it is a good
idea to abstract your audio file loading program into a library that imports all
the required files with a minimal amount of code duplication. In this chapter,
you will learn three new object creation patterns to help you do this. The first
pattern, called factory, is used to create your audio loader library. The second pat-
tern, called constructor, is introduced primarily because of its prevalence in the
JavaScript world, making it an important pattern to familiarize yourself with.
The third pattern is called class and is a syntax that is used in modern JavaScript
as an alternative to the constructor pattern. Factories, constructors are classes are
almost identical. The difference lies solely in minor implementation details and
syntax. In other words, anything you can do with one of these patterns you can
do with the other. Your choice of which to use comes down to personal choice.
In the next chapter, you will put what you learn here to work and build your
audio file loading library.
You can use factories to set properties and methods on the objects they return.
In the following example, the factory makeRecord is used to create objects
that represent music albums. With factories, property values are assigned to
the returned object through function arguments. In the following example, the
object’s property values represent information about each record, including title,
artist, and year.
function makeRecord(title, artist, year) {
let record = {};
record.title = title;
record.artist = artist;
record.year = year;
return record;
}
let weAreHardcore = makeRecord("We Are Hardcore", "The Psycho
Electros", 2030);
console.log(weAreHardcore.title); // "We Are Hardcore"
console.log(weAreHardcore.artist); // "The Psycho Electros"
console.log(weAreHardcore.year); // 2030
Private Data
Sometimes you want to create data that is accessible to your objects but is either
inaccessible to the outside scope or cannot be changed. To do this, you can make
data private by assigning it to a variable inside the factory. In the following exam-
ple, a variable named id stores some private information.
function makeRecord(albumId) {
let id = albumId; // Private data
console.log(id + " is private data");
let record = {};
return record;
}
let myRecord = makeRecord("2323415432");
console.log(myRecord.albumId); /*undefined. This is a property of the
object, not the private data!*/
Conversely, methods that are used to modify private data are called setters. In the
following code, a setter is created that allows you to change the value of id while
restricting the input to a ten-digit string.
Programming with factories is a common pattern in JavaScript and one that you
should be sure to familiarize yourself with. Factories give you a simple syntax
for abstracting complex code, while offering you the privacy of function scope
coupled with the flexibility of object extension.
As you can see, there are some differences between factories and constructors.
The first is the naming convention for functions. With constructors, it is con-
sidered good practice to name them with a capitalized noun. This convention
exists solely to help distinguish constructors from non-constructors and does not
throw an error if it is not used. The lack of an explicitly created object is the next
difference. With constructors, instead of immediately creating an object in your
function declaration, begin by writing your properties using the this keyword.
In a constructor, this points to the object that is created from it. These proper-
ties are assigned values through the constructor function arguments or, if you
want to create default values, you can assign them directly to the property.
You invoke a constructor using the new keyword. This is the command that tells
the interpreter that you are using the function as a constructor. In response, the
interpreter creates and returns an object. In the previous example, the return
value is assigned to the variable named weAreHardcore.
Admittedly, this syntax is a bit odd looking. So, to clarify what is happening, let’s
look at two concepts interwoven with constructors: the prototype object and the
prototype property.
Although you can attach your methods without using the prototype property,
the drawback to this approach is that every time you create a new object, all of
the methods are initialized, and this requires more memory. This might have
been a concern in 1995 when JavaScript was designed and computers were
much slower, but the large amount of available memory in modern computers
makes this issue negligible. This is the reason factories are a viable alterna-
tive. The syntax for adding methods without using the prototype object looks
like this:
You can use getters and setters, as you do with factories, to work with private
data in constructors. The following example contains a private variable named
id and uses a getter to retrieve it, as well as a setter that allows it to be changed
to a ten-digit string. Note that the getter and setter are not implemented on
the prototype property, because if they were, the private data would not be
available to them.
function Record(albumId) {
let id = albumId;
this.getId = function() {
return id;
};
this.setId = function(newId) {
if (typeof newId === "string" && newId.length === 10) {
id = newId;
} else {
throw ("id must be a ten digit string");
}
};
}
let myRecord = new Record("9876543210");
myRecord.getId(); // 9876543210
myRecord.setId("0123456789");
myRecord.getId(); //0123456789
Classes
The final object creation pattern discussed in this chapter is the class pattern.
Classes are a later edition to the language and were added in 2015. I have reserved
the class pattern to explain last because factories and constructors make apparent
what classes do behind the scenes. If you understand factories and constructors,
classes are easier to grasp
A Class Example
To demonstrate class syntax, we convert the previous constructor example
named Record into a class. The example is below.
First, we start with the constructor version.
// Constructor
function Record(title, artist, year) {
this.title = title;
this.artist = artist;
this.year = year;
}
Record.prototype.summary = function() {
return "Title:" + this.title + ". Artist:" + this.artist + ".
Year:" + this.year;
};
Next, we create a version of the previous code using the equivalent class syntax.
class Record{
constructor(title, artist, year){
this.title = title;
this.artist = artist;
this.year = year;
}
summary(){
return "Title:" + this.title + ". Artist:" + this.artist +
". Year:" + this.year;
}
}
When creating a class, declare the class by writing the class keyword. You then
type the name of the class and write a pair of opening and closing curly braces to
encapsulate the class body.
Class Record {
}
The next step is to compose the constructor method of the class. In a class, the
constructor method is where you write object properties. The constructor method
To create a new object, you do so by invoking the new keyword and using the
same syntax as the constructor pattern.
let weAreHardcore = new Record("We Are Hardcore", "The Psycho
Electros", 2030);
console.log(weAreHardcore);
/*
{
title: 'We Are Hardcore',
artist: 'The Psycho Electros',
year: 2030
}
*/
To create methods, you place them in the body of the class as shown in the exam-
ple below.
class Record{
constructor(title, artist, year){
this.title = title;
this.artist = artist;
this.year = year;
}
summary(){ // class method syntax
return "Title:" + this.title + ". Artist:" + this.artist +
". Year:" + this.year;
}
}
To retrieve or change private property values, you must use specialized meth-
ods prefaced with the keywords get and set. The following example contains a
method named currentId that is prefaced with the get keyword to retrieve the
private #id information.
class Record{
#id;
constructor(recordId){
this.#id = recordId
}
get currentId() {
return this.#id;
}
}
The code below demonstrates how to change private properties using methods
prefaced with the set keyword.
class Record{
#id;
constructor(recordId){
this.#id = recordId
}
get currentId() {
return this.#id;
}
set createNewId(newId){
this.#id = newId;
}
}
app.createNewId = "xyz123"
console.log(app.currentId); // xyz123
Now that you are familiar with factories and constructors from the previous
chapter, you can abstract the audio buffer loader you created in Chapter 11 into
a library that loads multiple sound files using less code. You do this using the
factory pattern.
With this approach, a factory function takes an object as an argument. The object
you input into the factory contains a list of property names, each of which is
assigned a directory of an audio file in the form of a string. The beauty of this
approach is its clarity and extensibility. The interface shown in sound.snare.
play() attempts to read, somewhat like English, from the list of sound files to
play. Even if you have never seen this code before, you can understand what it is
doing: selecting a sound in a specified directory and playing it. Decoupling the
object that contains many audio files from the invoking function makes the code
easier to read, as shown in the following example:
let audioFiles = {
kick: "kick.mp3",
snare: "snare.mp3",
hihat: "hihat.mp3",
shaker: "shaker.mp3"
//______hundreds of audio files could be listed here.........
};
let sound = audioBatchLoader(audioFiles);
sound.snare.play(); // Play
If the user of your abstraction decides they want to extend it to do new things,
without having to modify the source code in the original function, they have
some flexibility. So for example, if they wanted to extend the returned object to
play multiple audio buffers, they could do this:
sound.playSnareAndShaker = function() {
sound.snare.play();
sound.shaker.play();
};
sound.playSnareAndShaker(); // plays two sounds at the same time
with one line of code
};
getSound.send();
playSound.stop(audioContext.currentTime + time ||
audioContext.currentTime);
};
return soundObj;
function audioBatchLoader(obj) {
for (let prop in obj) {
obj[prop] = audioFileLoader(obj[prop]);
}
return obj;
}
The below code is used to load and play sound files and can be placed in app.js
let sound = audioBatchLoader({
kick: "sounds/kick.mp3",
snare: "sounds/snare.mp3",
hihat: "sounds/hihat.mp3",
Create a folder named sounds. This is the directory used to hold your audio files.
You can use your own audio files or use the audio files included in the exam-
ple code for this chapter.
Run sublime server and click the opening page. The audio file named snare.
mp3 will play. If you do not include the sound files, you will get an error.
You can now create the XMLHttpRequest object and set all the required prop-
erties and methods. You can also implement the decodeAudioData method to
make the buffer usable by the Web Audio API. These lines of code should already
be familiar to you because they are the same buffer loading and decoding tools you
learned about in Chapter 11, with one small difference. In Chapter 11, the decoded
buffer was assigned to a variable named audioBuffer. In this implementation,
the decoded buffer is assigned to a property of soundObj named soundBuffer.
function audioFileLoader(fileDirectory) {
let soundObj = {};
let playSound = undefined;
let getSound = new XMLHttpRequest();
soundObj.fileDirectory = fileDirectory;
getSound.open("GET", soundObj.fileDirectory, true);
getSound.responseType = "arraybuffer";
getSound.onload = function() {
audioContext.decodeAudioData(getSound.
response, function(buffer) {
soundObj.soundToPlay = buffer; // Property assigned buffer
});
};
getSound.send();
return soundObj;
}
You can now create a playback method that is an extension of soundObj to play
back the buffers.
function audioFileLoader(fileDirectory) {
let soundObj = {};
let playSound = undefined;
soundObj.fileDirectory = fileDirectory;
let getSound = new XMLHttpRequest();
getSound.open("GET", soundObj.fileDirectory, true);
getSound.responseType = "arraybuffer";
getSound.onload = function() {
audioContext.decodeAudioData(getSound.
response, function(buffer) {
soundObj.soundToPlay = buffer;
});
};
getSound.send();
soundObj.play = function(time) {
playSound = audioContext.createBufferSource();
playSound.buffer = soundObj.soundToPlay;
playSound.connect(audioContext.destination);
playSound.start(audioContext.currentTime + time ||
audioContext.currentTime);
};
return soundObj;
}
The stop method lets users determine when a sound will stop playback.
const audioContext = new AudioContext();
function audioFileLoader(fileDirectory) {
let soundObj = {};
let playSound = undefined;
soundObj.fileDirectory = fileDirectory;
let getSound = new XMLHttpRequest();
getSound.open("GET", soundObj.fileDirectory, true);
getSound.responseType = "arraybuffer";
getSound.onload = function() {
audioContext.decodeAudioData(getSound.response,
function(buffer) {
soundObj.soundToPlay = buffer;
});
};
getSound.send();
soundObj.play = function(time) {
playSound = audioContext.createBufferSource();
playSound.buffer = soundObj.soundToPlay;
playSound.connect(audioContext.destination);
playSound.start(audioContext.currentTime + time ||
audioContext.currentTime);
};
soundObj.stop = function(time) {
playSound.stop(audioContext.currentTime + time ||
audioContext.currentTime);
}
return soundObj;
}
One way to mitigate this additional repetition is to create a helper function that
loops through an object that contains a collection of audio file directories and
invokes the audioFileloader on each file. You can then return the object.
This will allow each sound to be accessible via its property name. The following
code demonstrates this:
function audioBatchLoader(obj) {
for (let prop in obj) {
obj[prop] = audioFileLoader(obj[prop]);
}
return obj;
}
You now have a working library to load multiple audio files. The following code
sets an event listener on the window. If you click it, you will hear the loaded
sound play.
let sound = audioBatchLoader({
kick: "sounds/kick.mp3",
snare: "sounds/snare.mp3",
hihat: "sounds/hihat.mp3",
shaker: "sounds/shaker.mp3"
});
window.addEventListener("mousedown", function() {
sound.snare.play();
});
Up to this point, the topic of the node graph has only been partially described and
has been used mostly as a tool to explain related concepts. In this chapter, you
will learn how to work with the node graph to develop custom signal chains for
complex audio applications. The Web Audio API includes many built-in objects
that let you manipulate audio in creative ways. You also learn how to include
these objects in your applications and use them to create customized effects.
Gain Nodes
In a real-world recording studio, you typically use a sound mixer with multiple
channel strips and a routing matrix to split and combine audio signals. With
the Web Audio API node graph, you use gain nodes to split and combine input
sources. Gain nodes allow independent volume control over input sources and act
as virtual mixing channels.
The final step is to define any additional properties or methods to customize the
effect. Properties or methods that allow you to customize the behavior of nodes
are called audio params (short for audio parameters). In the previous example,
type is an audio param. The following code sets another audio param named
frequency to the value 250. This defines where the low-pass filter begins to cut
off in the frequency spectrum.
const audioContext = new AudioContext();
let osc = audioContext.createOscillator();
osc.start(audioContext.currentTime);
let filter = audioContext.createBiquadFilter();
filter.type = "lowpass"; // audio param
filter.frequency.value = 250; // audio param
osc.connect(filter);
filter.connect(audioContext.destination);
The Problem
If you want to create independent node graphs using the audio loader abstraction
you can do so by accessing the buffers directly. The downside to this approach is
that playing them back requires some code duplication.
let sound = audioBatchLoader({
snare: "sounds/snare.mp3", // <---remember these are buffers!
kick: "sounds/kick.mp3",
window.addEventListener("mousedown", function() {
playSnare();
});
To fix this problem, we will make a small change to our library ensuring each
loaded audio file can reference its own custom node graph and to do so using
less code than the previous example. The update will introduce a new custom
method named connect. Our connect method is not to be confused with the
built-in connect() method of the Web Audio API and is used for our library
specifically. The use case of our newly created connect method is expressed in the
following example.
window.addEventListener("mousedown", function() {
sound.snare.connect(customNodeGraph).play();
});
One thing to keep in mind is that even though this update makes individualized
node graphs easier, there are times when you might need to create a node graph
});
};
getSound.send();
let nodeGraph;
soundObj.connect = function(callback) {
if (callback) {
nodeGraph = callback;
}
return {
play: soundObj.play,
stop: soundObj.stop
}
}
//___________________END update
soundObj.play = function(time) {
playSound = audioContext.createBufferSource();
playSound.buffer = soundObj.soundToPlay;
playSound.connect(audioContext.destination); // Remove
this line
playSound.start(audioContext.currentTime + time ||
audioContext.currentTime);
//_____________________________END update
};
soundObj.stop = function(time) {
playSound.stop(audioContext.currentTime + time ||
audioContext.currentTime);
};
return soundObj;
}
function audioBatchLoader(obj) {
for (let prop in obj) {
obj[prop] = audioFileLoader(obj[prop]);
}
return obj;
}
The explanation of the code is as follows. We first attach a method named con-
nect to the soundObj object. The connect method takes a callback as a parameter
and has a body with a conditional statement checking for the existence of the
callback. When the callback is passed, it is assigned to a variable in a higher
scope named nodeGraph. If the callback is omitted, the else branch of the condi-
tional statement is run and the connect method simply returns the play and stop
methods previously implemented. The code contained within the else branch
ensures the sound will still play and stop as expected even if our connect method
is invoked with no callback.
let nodeGraph;
soundObj.connect = function(callback) {
if (callback) { // If callback is passed in assign it to
variable in higher scope named nodeGraph.
nodeGraph = callback;
}
return { // If callback is omitted then simply
return play and stop methods.
play: soundObj.play,
stop: soundObj.stop
}
}
/* If the following code is run, the sound will still play even
though the callback is omitted from the connect method. */
sound.snare.connect().play() ;
The next change is an update to the play method. In this part of the code, you create
a conditional statement checking that the nodeGraph variable is assigned a value.
if (nodeGraph) {
return nodeGraph(playSound);
} else {
return playSound.connect(audioContext.destination);
}
//_____________________________END update
};
window.addEventListener("mousedown", function() {
sound.snare.connect(customNodeGraph).play();
});
Summary
In this chapter, you added additional flexibility to your audio loader library and
in the process, you were exposed to a real-world example of how callback func-
tions can be useful when designing a library. In the next chapter, you will con-
tinue to learn about node graphs.
Summary 165
16 The Biquad
Filter Node
Once you create the object, you can connect an input source to it. The following
example connects an oscillator to the object.
let audioContext = new AudioContext();
let osc = audioContext.createOscillator();
Filter Types
BiquadFilter contains a property named type that defines the type of filter
the node behaves like. If you do not explicitly set the type property, its default
value is lowpass. You can see this in the console.log() output in the fol-
lowing code:
const audioContext = new AudioContext();
let osc = audioContext.createOscillator();
let filter = audioContext.createBiquadFilter();
filter.frequency.value = 250;
console.log(filter.type); // default is lowpass
osc.connect(filter);
osc.start(audioContext.currentTime);
filter.connect(audioContext.destination);
To explicitly set the type property to lowpass, you write the following code:
filter.type = "lowpass";
Creating an Equalizer
Two of the most common types of equalizers are parametric and graphic. A
graphic equalizer allows you to boost or attenuate a series of fixed frequencies
but does not include the ability to modify the bandwidth of those selected fre-
quencies. Parametric equalizers, on the contrary, allow you to select a specific
frequency, boost or attenuate it, and change the bandwidth range. You can use
BiquadFilter nodes to design either of these equalizers, and many others.
Graphic EQ
The following diagram and code show how to create a seven-band graphic equal-
izer. You do this by chaining a series of BiquadFilter nodes together and set-
ting their type properties to peaking. Keep in mind that the only parameter
the user of a graphic equalizer should be allowed to change is the gain of each
filter. The input and output source for this example is abstracted using a function
named multibandEQ.
Graphic EQ 171
inputConnection.connect(filter1);
filter1.connect(filter2);
filter2.connect(filter3);
filter3.connect(filter4);
filter4.connect(filter5);
filter5.connect(filter6);
filter6.connect(filter7);
filter7.connect(outputConnection);
}
The code files for this chapter include versions of both the graphic and paramet-
ric equalizers with user interface controls. These applications allow you to toggle
the playback of a song and change parameters of the BiquadFilter nodes in
real time by using the interactive sliders.
Parametric EQ
You can design a parametric equalizer in a similar way to the graphic equal-
izer by chaining a series of BiquadFilter nodes together and setting their
type properties to peaking. The primary difference of the parametric equalizer
is that the frequency, gain, and bandwidth are modifiable by the user. Keep in
mind that with multiband parametric equalizers, the filter type may have mul-
tiple options available. To keep the code simple and short, the following example
shows how to create a single-band parametric equalizer with type set to the value
peaking. The input and output source in this code is abstracted using a func-
tion named parametricEQ.
Summary
In this chapter, you learned about the BiquadFilter node and how to use it to
create custom equalizers and filter arrangements. Keep in mind that the exam-
ples here are kept simple, and like the node graph itself, your filter arrangements
can be as complex as you want to make them. In the next chapter, you will learn
about another signal processing node: the convolver node.
Summary 173
17 The
Convolver
Node
In this chapter, you will learn how to use the convolver node. The convolver
allows you to apply reverberation to node graph input sources by referencing a
special kind of audio file called an impulse response.
Convolution Reverb
When an acoustic sound is created, its characteristics are shaped by its immedi-
ate environment. This is due to sound waves bouncing off and around various
obstacles. These obstacles can be made of different materials that affect the sound
in different ways. The result of sound emanating from a small room has different
characteristics than sound emanating from a large room. Because the human
ear can hear these differences, when this information is transmitted to the brain,
we perceive these characteristics as room ambience. Modern advancements in
digital audio technology allow us to record the ambience of any real-world envi-
ronment and apply it to any digital audio signal directly. These recorded ambi-
ences are stored as a special file called an impulse response. An impulse response
file is made by recording a single sound burst in an environment, which could
be white noise, a sine wave sweep, or even a balloon pop. This recording is then
run through a special digital algorithm to create a single file called an impulse
response. This impulse response file is combined or convolved with another input
After the file is stored in a buffer, the next step is to wire up the necessary nodes
to apply the effect to an input source. To integrate the impulse response into
the node graph configuration, you must first create a convolver node using
audioContext.createConvolver() and store the returned object in a
variable.
let convolver = audioContext.createConvolver();
You then assign the loaded impulse response buffer to the buffer property of the
object.
convolver.buffer = impulseResponseBuffer;
Next, you connect any input source you want to the convolver node. Here is an
example of connecting an oscillator.
The following HTML and JavaScript code combine the impulse file loader,
node graph connections, and JQuery DOM selectors to allow you to play the
oscillator by clicking an HTML button and holding it. This allows you to hear the
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript" src="https://ajax.googleapis.
com/ajax/libs/jquery/3.6.0/jquery.min.js "></script>
<script src="js/app.js"></script>
</head>
<body>
<button>Oscillation</button>
</body>
</html>
JavaScript
"use strict";
const audioContext = new AudioContext();
let impulseResponseBuffer;
let getSound = new XMLHttpRequest();
getSound.open("get", "sounds/impulse.wav", true);
getSound.responseType = "arraybuffer";
getSound.onload = function() {
audioContext.decodeAudioData(getSound.response, function(buffer) {
impulseResponseBuffer = buffer;
});
};
getSound.send();
/*___________________________________________BEGIN playback
functionality*/
let osc = audioContext.createOscillator();
function playback() {
let convolver = audioContext.createConvolver();
osc = audioContext.createOscillator();
osc.type = "sawtooth";
convolver.buffer = impulseResponseBuffer;
osc.connect(convolver);
convolver.connect(audioContext.destination);
osc.start(audioContext.currentTime);
}
$(function() {
$("button").on("mousedown", function() {
playback();
});
$("button").on("mouseup", function() {
osc.stop();
JavaScript 177
});
});
Summary
In this chapter, you learned how to use the convolver node to apply an impulse
response file to an input source. You also learned how to use gain nodes to control
the amount of the effect you want to hear. In the next chapter, you will learn how
to modify the panning of stereo input sources and how to create sophisticated
routing schemes using the channel and merger nodes.
You can then connect any input source to the node and use pan.value to set
the location in the stereo field where you want to place the sound. The pan.
value property setting is a number between 1 and −1, where 1 represents a 100
percent pan to the right and −1 represents a 100 percent pan to the left. In the
following example, an oscillator is connected to a stereo panner node and is set
50 percent to the left.
let oscillator = audioContext.createOscillator();
let stereoPanner = audioContext.createStereoPanner();
To use the channel splitter, you connect input sources to it and then connect the
splitter to other nodes. When connecting the splitter to a destination node, you
specify the channel of the input source to connect to in the second argument of
the connect() method. This argument is a number that represents the channel
as an index value. The following chart displays the index for each channel of a
six-channel input source.
The following code shows the correspondence between the channel index argu-
ment and its respective channel type:
stereoInputSource.connect(splitter);
splitter.connect(audioContext.destination, 0); /*outputs left
side/channel of stereo input source*/
splitter.connect(audioContext.destination, 1); /*outputs right
side/channel of stereo input source*/
When connecting an input source to a channel merger, you must specify the out-
put channel using the third argument of the connect method.
inputSource.connect(merger, 0, 1); /*outputs all channels of
inputSource to right channel*/
Merging All Channels of a Multichannel File into a Single Mono Channel 181
let multiChannelInputSource = audioContext.createBufferSource();
let merger = audioContext.createChannelMerger(1); /*Set number of
channels*/
stereoInputSource.buffer = audioBuffer;
stereoInputSource.connect(merger);
merger.connect(audioContext.destination);
If you connect an audio input source, such as an audio buffer source node,
directly to a channel merger node, there is no reason to set the second argument
of the connect method to a value other than 0. This is because the merger node
has a single output.
audioBufferSource.connect(merger, 0, 1);
If the input of a channel merger is a channel splitter, the second argument of the
connect method is the channel of the input source sent to the merger.
let channelSplitter = audioContext.createChannelSplitter();
let channelMerger = audioContext.createChannelMerger();
let sound = audioContext.createBufferSource();
sound.buffer = audioBuffer;
sound.connect(channelSplitter);
channelSplitter.connect(channelMerger, 0, 0); /*The left channel
of playSound is connected to the channel merger*/
channelMerger.connect(audioContext.destination);
Summary
In this chapter, you learned how to apply stereo panning to audio input sources.
You also learned how to work with the channel splitter and channel merger
nodes. In the next chapter, you will explore how to create delay effects using the
delay node.
In the world of creative audio, delays are a common method used to create time-
based effects. In this chapter, you will learn how to use the delay node to create
the most common delay effects: echo, slap back, and ping-pong.
If you listen to the result of the previous example, you will notice that it does not
provide the repetitive echo delay effect that is typical of an effects processor. This
is because the only thing the delay node does is pause the audio from playing for
a set amount of time. If you want a repetitive echo effect, you must create it.
The gain.value property controls the amount of the effect and the
delayTime.value property controls the length of the delay. The following
code applies the effect to an audio buffer.
let sound = audioContext.createBufferSource();
let delayAmount = audioContext.createGain();
let delay = audioContext.createDelay();
sound.buffer = audioBuffer;
delay.delayTime.value = 0.5;
delayAmount.gain.value = 0.5;
sound.connect(delay);
delay.connect(delayAmount);
delayAmount.connect(delay);
delayAmount.connect(audioContext.destination);
sound.connect(audioContext.destination);
sound.start(audioContext.currentTime);
The following code implements the configuration shown in the above figure:
//___________________________________________________BEGIN setup
let sound = audioContext.createBufferSource();
sound.buffer = audioBuffer;
let merger = audioContext.createChannelMerger(2);
let splitter = audioContext.createChannelSplitter(2);
let leftDelay = audioContext.createDelay();
let rightDelay = audioContext.createDelay();
let leftFeedback = audioContext.createGain();
let rightFeedback = audioContext.createGain();
//____________________________________________________END setup
sound.connect(splitter);
sound.connect(audioContext.destination);
splitter.connect(leftDelay, 0);
leftDelay.delayTime.value = 0.5;
leftDelay.connect(leftFeedback);
leftFeedback.gain.value = 0.6;
leftFeedback.connect(rightDelay);
splitter.connect(rightDelay, 1);
rightDelay.delayTime.value = 0.5;
rightFeedback.gain.value = 0.6;
rightDelay.connect(rightFeedback);
rightFeedback.connect(leftDelay);
leftFeedback.connect(merger, 0, 0);
rightFeedback.connect(merger, 0, 1);
//___________________________________________END output
sound.start(audioContext.currentTime);
Summary
The delay node by itself is not complicated or difficult to use, but when combined
with other nodes it can be a powerful tool for the creation of interesting audio
effects. In the next chapter, you will continue exploring the node graph and learn
how to apply dynamic range compression to audio input sources.
In this chapter, you will learn about the dynamics compressor node. This node
allows you to apply dynamic range compression to audio input sources.
The object provides you with a collection of five properties that affect the dynamic
range of an audio input source. A sixth property called reduction is also avail-
able, but it does not affect the input source in any way. The reduction property is
Property Description
Threshold The decibel value above which the compression will start taking effect. Its default
value is −24, with a nominal range of −100 to 0
Ratio Determines how much compression is administered. Setting the ratio to 2 means
that for every 2 dB that the signal exceeds the threshold there will be only 1 dB in
amplitude change. The ratio property takes a number between 1 and 20
Knee A decibel value representing the range above the threshold where a curve is created
that smoothly transitions to the compressed part of the signal. Its default value is
30, with a nominal range of 0–40
Release Sets the release speed of the compression effect. The amount of time (in seconds)
to reduce the gain by 10 dB. Its default value is 0.003, with a nominal range of 0–1
Attack Sets the attack speed of the compression effect. The amount of time (in seconds) to
increase the gain by 10 dB. Its default value is 0.250, with a nominal range of 0 to 1
Reduction A numeric readout of the reduction being applied. The reduction property does
not affect the signal and is used for metering purposes
The following code demonstrates how to apply the dynamics compressor node
to an audio input source. In this example, for every 12 dB the signal surpasses a
threshold of −40 dB, its output is increased by 1 dB.
//___________________________________________________BEGIN setup
let sound = audioContext.createBufferSource();
let compressor = audioContext.createDynamicsCompressor();
sound.buffer = audioBuffer;
//____________________________________________________END setup
sound.connect(compressor);
compressor.threshold.value = -40;
compressor.ratio.value = 12;
//___________________________________________BEGIN output
compressor.connect(audioContext.destination);
//___________________________________________END output
sound.start(audioContext.currentTime);
Anyone familiar with the world of creative audio will immediately be famil-
iar with every property available to the dynamics compressor node except one:
reduction. The reduction property, specific to the Web Audio API, outputs
a numeric value representing the amount of reduction the compressor is impos-
ing on the input source. The following code uses setInterval() to allow you
to see the change in reduction value as an audio input source is compressed.
//____________________________________________________END setup
sound.connect(compressor);
compressor.threshold.value = -40;
compressor.ratio.value = 12;
//___________________________________________BEGIN output
compressor.connect(audioContext.destination);
//___________________________________________END output
sound.start(audioContext.currentTime);
window.setInterval(function() {
console.log(compressor.reduction);
}, 50);
Summary
Using the dynamics compressor node is not complicated. It contains all the basic
parameters needed to modify the dynamic range of any input source connected
to it.
In the next chapter, you will learn how to work with time in the Web Audio
API.
Summary 189
21 Time
In this chapter, you will learn how to work with time to schedule Web Audio
API sound playback points, how to create loops, and how to automate parameter
changes.
When you play an audio event, the Web Audio API requires you to schedule it.
Remember that the unit you use for time value scheduling is seconds. If you want
to schedule an event immediately, you can use the currentTime property of
the audio context.
You have already had some exposure to scheduling the playback of sounds in
previous chapters, such as in the following example code:
The third argument sets how much of the sound will play. If you have a sound
that is 4 seconds long and you only want to hear the first 2 seconds, then you set
the third argument to 2.
sound.start(audioContext.currentTime,0, 2);
Looping Sounds
To loop sounds, you set the loop property of an audio buffer source node to
true. To set the start point of a loop, you use the property loopStart. To set
the end point of a loop, you use the property loopEnd.
sound.loop = true;
sound.loopStart = 1; /*Set loop point at one second after
beginning of playback*/
sound.loopEnd = 2; /*Set loop end point at two seconds after
beginning of playback*/
Sometimes when trying to discern playback and loop points, it is useful to know
the length of an audio file. You can get this information using a property of the
sound buffer named duration.
let sound = audioContext.createBufferSource();
sound.buffer = buffer;
sound.buffer.duration; // length in seconds of audio file
Included in the code examples for this chapter is an application that allows you to
modify the playback and loop points of an audio file in real-time using interac-
tive sliders.
playSound.buffer = soundObj.soundToPlay;
playSound.start(audioContext.currentTime + time || audioContext.
currentTime, setStart || 0, setDuration || soundObj.
soundToPlay.
duration);
if (nodeGraph) {
return nodeGraph(playSound);
} else {
return playSound.connect(audioContext.destination);
}
};
You can use these methods in place of setting the value property of an audio
parameter.
osc.frequency.value = 100; // Set value directly
osc.frequency.setValueAtTime(arg1,arg2); /*Set value with audio
parameter method*/
To use any of the other audio parameter methods that are described next, you
must first initialize their settings using setValueAtTime(). This is shown in
the code examples for each method.
The second argument represents when you want the changes to begin, and the
third argument is the time span you want the changes to take place within. The
following code demonstrates this by toggling the frequency of an oscillator from
100 to 500 Hz and back again over the course of 3 seconds. This creates a wobble
effect.
let waveArray = new Float32Array(10);
waveArray[0] = 100;
waveArray[1] = 500;
waveArray[2] = 100;
waveArray[3] = 500;
waveArray[4] = 100;
waveArray[5] = 500;
waveArray[6] = 100;
waveArray[7] = 500;
waveArray[8] = 100;
waveArray[9] = 500;
let osc = audioContext.createOscillator();
let volume = audioContext.createGain();
osc.frequency.value = 500;
osc.frequency.setValueAtTime(osc.frequency.value, audioContext.
currentTime); // Set initial values
osc.frequency.setValueCurveAtTime(waveArray, audioContext.
currentTime + 1, 3);
osc.start(audioContext.currentTime);
osc.connect(audioContext.destination);
Summary
In this chapter, you learned the fundamentals of working with time. You learned
how to loop and schedule sound playback, as well as how to schedule parameter
value changes. In the next chapter, you will learn how to create audio visualiza-
tions using the Analyser node.
In this chapter, you will learn how to use the Analyser node to create a spec-
trum analyzer that displays real-time amplitude information of audio signals
across a collection of frequency bands. The Web Audio API includes a node
named Analyser that gives you real-time frequency and time domain infor-
mation about audio input sources. This information can be used to create custom
visual representations of audio signals that include (but are not limited to) spec-
trum analyzers, phase scopes, and waveform renders.
When all bits are on, a byte has a value of 255. This allows for 256 total pos-
sible values (0–255).
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript" src="js/jquery.js"></script>
<script src="js/app.js"></script>
<link rel="stylesheet" href="css/app.css" type="text/css">
</head>
<!--______________________________________________________BEGIN
HTML 199
APP-->
<body>
<p class="bin-count">
Bin count:<b class="bin-count-number"></b>
</p>
<div class="app">
</div>
</body>
<!--______________________________________________________END
APP-->
</html>
CSS
.app{
position: relative;
margin: 10px;
}
.app > div {
width: 0.1px;
background-color: orange;
display: inline-block;
outline-style:solid;
outline-color:orange;
outline-width: 0.1px;
margin-left:8px;
}
span{
display:inline-block;
font-size:14px;
color:rgba(128, 128, 128, 0.5);
margin:2px;
}
.bin-count{
position:absolute;
left:30%;
float:right;
font-size:2em;
height:50px;
}
The Analyser interface enables you to perform various FFTs on the audio
stream. The FFT used to create a spectrum analyzer transforms the time domain
of the audio signal into normalized (or limited) frequency-domain data. This is
done by chopping the original audio signal into parts, typically called bins, and
performing an analysis and transformation on each part.
The size of the FFT is stored in the fftSize property of the Analyser
node and the default value is 2048. The allowed values are any power of 2 between
32 and 2048. If you set it wrong, you will get an error. The number of bins avail-
able is one half of the fftSize property and is accessible by a read-only prop-
erty of the Analyser node named frequencyBinCount.
Each bin is designated a range of frequencies called a band, and the following
formula determines the range of each band:
Example:
44,100/2048 = 21.533203125
There are two kinds of typed arrays that the Analyser node is designed to work
with: Float32Array and Uint8Array. The index values of a Float32Array
are always a decimal number between 0 and 1. The index values of a Uint8Array
are limited to 8 bits of information and will always be an integer between 0 and 255.
Using a Float32Array allows for up to 32 bits of information and gives you more
precision but is more resource intensive. This is in contrast to Uint8Array, which
is more resource efficient but less precise. This application uses a Uint8Array. A
Float32Array or Uint8Array must be created using the keyword new.
In the following code, the Uint8Array is invoked with a single argu-
ment that determines the number of indexes it will have by using analyzer.
frequencyBinCount.
let frequencyData = new Uint8Array(analyzer.frequencyBinCount);
console.log(frequencyData.length); // 1024
You now store the frequency domain data in the array using
getByteFrequencyData().
analyzer.getByteFrequencyData(frequencyData);
The bars variable selects all div elements and is used later in the code.
bars = $(".app > div");
To create the vertical frequency bars, the for loop updates the CSS height prop-
erty of each div stored in the bar variable. The value given to each div is a
The user interface of the spectrum analyzer is designed to create a div for all
bins. You can change the size of the FFT to lower the bin count.
analyzer.fftSize = 64;
The application you created in this chapter works with frequency-domain data.
If you want to work with time-domain data, the Analyser node has two meth-
ods that let you copy it to a typed array. The first method is getByteTime
DomainData () and is for use with a Uint8Array(). The second method
is getFloatTimeDomainData () and is for use with Float32Array(). To
store the time domain data in a Uint8Array(), you write the following code:
let frequencyData = new Uint8Array(analyzer.frequencyBinCount);
analyzer.getByteTimeDomainData(frequencyData);
Summary
In this chapter, you learned about the Analyser node and created a frequency
spectrum analyzer application. In the next chapter, you will learn how to build
an interactive music sequencer.
Summary 205
23 Building
a Step
Sequencer
Music applications, like sequencers and drum machines, allow users to record,
edit, and playback sounds as a collection of organized note arrangements. Due
to the nature of the Web Audio API and its relationship to the DOM, music
sequencing applications are a challenge to create. In this chapter, you will learn
why this is so and how to meet the challenge by building a basic drum pattern
step sequencer.
The Problem
The Web Audio API lets you schedule events immediately or in the future. A
problem with this approach is that once an event is scheduled, it cannot be
unscheduled. So for example, the following code schedules three drum sounds to
play in an eighth note pattern for four bars.
let sounds = audioBatchLoader({
kick:"sounds/kick.mp3",
snare:"sounds/snare.mp3",
hihat:"sounds/hihat.mp3"
});
If you want to change the tempo relationship of these sounds in the middle of the
four bars, you can’t. Instead, you have to wait until the sounds have completed
playing. This is true of any scheduled events that you might want to change dur-
ing playback. And this restriction is not relegated to just tempo changes.
function playSequence(){
window.setInterval(function() {
if (counter === 8) {
counter = 1;
} else {
counter += 1;
}
if (counter) {
sounds.hihat.play();
}
if (counter === 3 || counter === 7) {
The problem with this approach is that both the setTimeout and setInt-
erval methods have timings that are imprecise and unstable. There are two
reasons for this. The first is that the smallest unit of time available to these meth-
ods is an integer of 1 millisecond, which is not precise enough for audio sample-
level values like 44.100 kHz. The other problem is that unlike the Web Audio
API timing clock, these methods can be interrupted by ancillary browser activity
like page rendering and redraws. Although you might expect setInterval or
setTimeout to run at every nth millisecond, depending on factors outside your
control, the value will likely be larger and audibly noticeable.
The Solution
The solution to the problem is to create a relationship between the Web Audio
API timing clock and the browser’s internal setTimeout method to create a
look-ahead mechanism that recursively loops and checks if events will be sched-
uled at some time in the future. If this is the case, the scheduling happens and
the event(s) takes place. This gives you enough leeway to cancel events at the last
moment if needed.
One thing to keep in mind is that because setTimeout is inherently
unstable, we know that this relationship will always have an unstable aspect to it.
Whether or not this approach is stable enough for your applications is for you to
decide. One thing we can be certain of is that it is much more accurate than using
setInterval or setTimeout on its own.
How It Works
The basis for the relationship between the Web Audio API timing clock and the
browser’s internal setTimeout method is expressed in the following code:
let audioContext = new AudioContext();
let futureTickTime = audioContext.currentTime;
function scheduler() {
if (futureTickTime < audioContext.currentTime + 0.1) {
futureTickTime += 0.5; /*_can be any time value. 0.5 happens to
be a quarter note at 120 bpm */
console.log(futureTickTime);
}
window.setTimeout(scheduler, 0);
}
The way the previous code works is that the setTimeout function loops
recursively, and upon each iteration, a conditional checks whether the value of
futureTickTime is within a tenth of a second of the audioContext.cur-
rentTime. If this evaluates to true then futureTickTime is incremented
by 0.5, which is half a second in “Web Audio Time”. The futureTickTime vari-
able remains set at this value until audioContext.currentTime “catches
up with it” once again. Then within a tenth of a second, futureTickTime is
incremented by a half-second into the future. This pattern continues for as long
as the function is allowed to run.
Because a half-second translates to a quarter note at 120 BPM, the following
code uses this information to create a 1/4th note timing count that is logged to
the console.
let audioContext
let futureTickTime;
let counter = 1;
function scheduler() {
if (futureTickTime < audioContext.currentTime + 0.1) {
futureTickTime += 0.5; /*_can be any time
value. 0.5 happens to be a quarter note at 120 bpm */
console.log("This is beat: " + counter);
counter += 1;
if (counter > 4) {
counter = 1;
}
}
window.setTimeout(scheduler, 0);
}
window.onclick = function () {
audioContext = new AudioContext();
futureTickTime = audioContext.currentTime;
scheduler();
}
function playMetronome(time) {
let osc = audioContext.createOscillator();
osc.connect(metronome);
metronome.connect(audioContext.destination);
osc.start(time);
osc.stop(time + 0.1);
}
function scheduler() {
if (futureTickTime < audioContext.currentTime + 0.1) {
playMetronome(futureTickTime);
futureTickTime += 0.5; /*_can be any time
value. 0.5 happens to be a quarter note at 120 bpm */
console.log("This is beat: " + counter);
counter += 1;
if (counter > 4) {
counter = 1;
}
}
window.setTimeout(scheduler, 0);
}
window.onclick = function () {
audioContext = new AudioContext();
futureTickTime = audioContext.currentTime;
Changing Tempo
If you want to change the tempo, you have to change the time relationship
between events. You can do this by altering when events are scheduled to start
with the futureTickTime variable. The following formula is useful for con-
verting beats (quarter notes) to seconds:
let tempo = 120.0; // tempo (in beats per minute);
let secondsPerBeat = (60.0 / tempo);
The application you build assumes the use of a 16th note grid. You can design
it with any beat division(s) you want, but for simplicity it is hard-coded with 16
notes. The following code converts the futureTickTime variable from a time
value that represents a quarter note to a time value that represents a 16th note.
The oscillator is also modified to play a different frequency on the downbeat.
"use strict";
let audioContext,
futureTickTime,
counter = 1,
metronome,
tempo = 90,
secondsPerBeat = 60 / tempo,
counterTimeValue = (secondsPerBeat / 4),
oscFrequency = 100,
osc;
function playMetronome(time) {
let osc = audioContext.createOscillator();
if (counter === 1) {
oscFrequency = 400;
} else {
oscFrequency = 100;
}
osc.connect(audioContext.destination);
osc.frequency.value = oscFrequency;
osc.start(time);
osc.stop(time + 0.1);
}
function scheduler() {
if (futureTickTime < audioContext.currentTime + 0.1) {
playMetronome(futureTickTime);
futureTickTime += counterTimeValue;
console.log("This is beat: " + counter);
window.onclick = function () {
audioContext = new AudioContext();
futureTickTime = audioContext.currentTime;
osc = audioContext.createOscillator();
metronome = audioContext.createGain();
scheduler();
You can now change the tempo by modifying the tempo variable.
</body>
<!--____________________________________________END APP-->
</html>
Inside the sequencer folder, create a folder named sounds and place audio files for
the sequencer application in it (you will need a hihat.mp3, kick.mp3, shaker.mp3,
and snare.mp3).
let futureTickTime,
counter = 1,
metronome,
tempo = 90,
secondsPerBeat = 60 / tempo,
counterTimeValue = (secondsPerBeat / 4),
oscFrequency = 100,
osc;
osc.connect(audioContext.destination);
osc.start(time);
osc.stop(time + 0.1);
function playTick() {
console.log("This is 16th note: " + counter);
counter += 1;
futureTickTime += counterTimeValue;
if (counter > 16) {
counter = 1;
function scheduler() {
if (futureTickTime < audioContext.currentTime + 0.1) {
playMetronome(futureTickTime, true);
playTick();
window.setTimeout(scheduler, 0);
The track arrays are populated with values so that you can hear a drum sequence
immediately.
"use strict";
let futureTickTime,
counter = 1,
metronome,
metronomeVolume = 1,
tempo = 90,
secondsPerBeat = 60 / tempo,
counterTimeValue = (secondsPerBeat / 4),
oscFrequency = 100,
osc;
/*_____________________________________________BEGIN load
sounds BOLD */
if (counter === 1) {
osc.frequency.value = 500;
} else {
osc.frequency.value = 300;
}
osc.connect(metronome);
metronome.connect(audioContext.destination);
osc.start(time);
osc.stop(time + 0.1);
}
}
function playTick() {
console.log("This is 16th note: " + counter);
counter += 1;
futureTickTime += counterTimeValue;
if (counter > 16) {
counter = 1;
}
}
function scheduler() {
if (futureTickTime < audioContext.currentTime + 0.1) {
playMetronome(futureTickTime, true);
scheduleSound(kickTrack, sounds.kick, counter, futureTickTime
- audioContext.currentTime);
scheduleSound(snareTrack, sounds.snare, counter, futureTickTime
- audioContext.currentTime);
scheduleSound(hiHatTrack, sounds.hihat, counter, futureTickTime
- audioContext.currentTime);
scheduleSound(shakerTrack, sounds.shaker, counter,
futureTickTime - audioContext.currentTime);
playTick();
}
}
window.onclick = function() {
futureTickTime = audioContext.currentTime;
osc = audioContext.createOscillator();
metronome = audioContext.createGain();
scheduler();
This is done because the audio library you built in Chapter 13 is designed to
reference audioContext.currentTime by default and adds any additional
numeric arguments to this value. You subtract audioContext.current-
Time from futureTickTime because these values will be combined when the
play() method of your library is invoked.
When scheduler() is invoked, the drum sequence does not start imme-
diately because it takes time for the audio buffers and files to load thus an event
listener invokes the scheduler function when the user clicks the page. In our app,
we will change this so that the scheduler is initiated by a play/stop button. In your
HTML code, create a button with a class of play-stop-button and give it the
text of play/stop.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>app</title>
<script src="https://ajax.googleapis.com/ajax/libs/
jquery/3.6.0/jquery.min.js"></script>
<script src="js/audiolib.js"></script>
<script src="js/app.js"></script>
<link rel="stylesheet" href="css/app.css">
</head>
<!--___________________________________________BEGIN APP-->
<body>
<!--HTML code-->
<button class="play-stop-button">
Play / Stop
</button>
</body>
<!--____________________________________________END APP-->
</html>
$(function() {
let futureTickTime,
counter = 1,
metronome,
metronomeVolume = 1,
tempo = 90,
secondsPerBeat = 60 / tempo,
counterTimeValue = (secondsPerBeat / 4),
oscFrequency = 100,
timerID,
isPlaying = false,
osc;
/*_____________________________________________BEGIN load
sounds*/
});
if (playing) {
osc.frequency.value = 500;
} else {
osc.frequency.value = 300;
osc.connect(metronome);
metronome.connect(audioContext.destination);
osc.start(time);
osc.stop(time + 0.1);
function playTick() {
console.log("This is 16th note:" + counter);
counter += 1;
futureTickTime += counterTimeValue;
if (counter > 16) {
counter = 1;
function scheduler() {
function play() {
osc = audioContext.createOscillator();
metronome = audioContext.createGain();
isPlaying = !isPlaying;
$(".play-stop-button").on("click", function() {
play();
});
});
If you launch this code from your server and click the play/start button, it will
start and stop the application.
</body>
<!--_______________________________________________END APP-->
</html>
CSS
The following code is the CSS for the application:
body{
background-color:red;
font-size:25px;
}
button{
margin-bottom:5px;
font-size:25px;
}
.track-step{
width:50px;
height:50px;
display:inline-block;
background-color:orange;
outline-style:solid;
outline-width:1px;
margin-left:5px;
}
CSS 221
To create the div elements for the grid, you use a nested JavaScript for loop.
Each collection of grid items has a parent container. Type the following for-loop
at the very bottom of your JQuery code.
$(function(){
// …. previous app code
});
The following code allows you to toggle the metronome on and off:
$(function(){
// ….... previous app code
$(".play-stop-button").on("click", function() {
play();
});
});
Next, you write code that lets users control the tempo from the HTML input
range slider and displays the current tempo on the web page. First, modify the
playTick() function:
Then create the event listener used to control the tempo from the slider:
$(".metronome").on("click", function() {
if (metronomeVolume.gain.value) {
metronomeVolume.gain.value = 0;
} else {
metronomeVolume.gain.value = 1;
}
});
$("#tempo").on("change", function() {
tempo = $(this).val();
$("#showTempo").html(tempo);
});
You can now modify the tempo of the sequence by moving the HTML input
slider.
JQuery has a method named index() that allows you to capture an element’s
index value relative to a parent element. In the case of the sequencer application,
the index value of the first grid-item of each row is 0 and the last grid-item
index is 15. You can give this value an offset of +1 so that the first index grid-item
is referenced as 1 and the last is referenced as 16. This allows for a correlation
between the grid-item index values and the counter value. You can capture
this information by setting an event listener to all elements with a class of grid-
item. When the user clicks the grid-item, the offset index value is either
pushed to or removed from a corresponding track array dependent on whether
the grid-item is active or not. This is what determines if a sound will play at a
certain point in the music sequence. The following code implements this feature
and also modifies the CSS background-color of the grid-item based on
whether it is active or not.
You can now run the sequencer and playback sounds by clicking the squares. The
tempo also changes when the slider is moved.
Summary 225
24 AJAX and
JSON
In this chapter, you are going to learn how to query data using web APIs and to
create your own web API for accessing synth patch data to use in a web audio
synthesizer. Third-party web services commonly allow a portion of their data
to be accessible via a web API. This gives you the ability to query data on their
server and use their data in your applications. An example is the iTunes public
search API that lets developers search media titles in the iTunes store. To begin,
you must first learn about two technologies: AJAX and JSON.
AJAX
AJAX is an acronym that stands for Asynchronous JavaScript and XML. This is
a technology that allows you to use JavaScript to make network requests and
access data asynchronously. You have already worked with AJAX in previous
chapters when loading audio buffers using the XMLHttpRequest object and
the fetch API. The X in AJAX refers to XML, which stands for Extensible Markup
Language. This was originally the data exchange format used with AJAX and is
rarely used now. In modern web development, the data exchange format you use
is JSON.
{
"buzzFunk": [{
"type": "sawtooth",
"frequency": 65.25
}, {
"type": "triangle",
"frequency": 65.25
}, {
"type": "sawtooth",
"frequency": 67.25
}]
}
You have just queried the iTunes search API for any music that includes the
keyword “funk” and are now in possession of a JavaScript object that contains
this data.
"https://itunes.apple.com/search?term=funk&media=music";
The part of the endpoint after the “?” symbol is called the query string. This part
of the URL contains the data that is being queried. The “&” symbol separates the
key/value pairs.
The iTunes API search terms are assigned to specific keys and in the previ-
ous code, these are term and media. There is no standardization across web
APIs for key/value names, and they are different for each web API. Because the
URL structure for all web APIs is different, you will need to read the documen-
tation for any that you are working with. The documentation for the iTunes
search API is here: https://affiliate.itunes.apple.com/resources/documentation/
itunes-store-web-service-search-api/.
The fetch API encapsulates callbacks inside a method named then. The first
callback returns the response data and, in our example, converts it to JSON for-
mat. The second callback is used to log the data to the console.
Node.js
The JavaScript that you’ve written up to this point has been limited to the context
of a web browser. Node.JS is a downloadable program that lets you run JavaScript
directly from the terminal on your computer. The terminal is a built-in applica-
tion that gives you access to the internal data of your entire computer. Node.Js
lets you write JavaScript in the terminal to manipulate files on your computer as
well as create and launch scripts, build web servers, and perform other actions. In
our case, we are going to use Node.JS to create a very basic web API that contains
synthesizer patch data.
To get started, you must first download and install Node.js. You can do so
by going to the following URL and following the directions for your operating
system. https://nodejs.org/en/.
Once the application is done installing it will require terminal access.
Windows OS terminal commands are slightly different than on Mac and Linux
Launching Node.JS
To check if Node.JS is installed correctly, launch your computer’s terminal and
type the word “node” into the prompt and press enter on your keyboard. If
you get a command saying command not recognized then NodeJS was not
installed. Otherwise, it is installed. Feel free to experiment by typing JavaScript
statements into the prompt such as 1 + 1. You execute your code by pressing the
enter key.
Getting Started
On your desktop (or another directory of your choice) create a folder named
server.
Open your terminal and type the command cd. The cd command stands
for current directory and lets you navigate between directories in the terminal.
After you type the letters cd, drag the app folder to the terminal. Dragging a
folder to the terminal is a way to reference its directory location without manu-
ally typing it out. Press enter on your keyboard.
In the terminal type npm init and press enter. You are presented with
a prompt that says “This utility will walk you through creating a package.json
file”. You are then asked a question as to the name of your application. You can
either type a name or press enter to provide a default answer and immediately
skip to the next question. Providing custom answers to any of the questions is not
required to create the application.
Once all prompted questions are complete a file named package.json will
appear in the server directory. This file contains default values for the application
you are creating.
Before we run the code, our terminal must be referencing the directory of app.js.
Open your terminal and go to the directory of your server folder by typing cd,
and then dragging the folder named server to the terminal. When you’re done,
press enter. To run the app.js file using Node.Js, you type the name of the file
prefaced with the node command as shown below.
node app.js
In the terminal, the string “Hello World” is displayed. When you are done open
app.js in your code editor, delete the code you wrote, and save the file.
"author": "",
"license": "ISC"
}
At the top of app.js in your server folder import the http module using this syntax:
import http from “http”;
In the terminal go to the server folder directory and type node app.js. Open
your browser and go to localhost:3000. You will see the text “Hello Sound API”.
To stop the program, select the terminal and press the CTRL and the C keys at
the same time.
For our purposes we do not want to write text to the client, we want to
respond with a JSON object. Update the previous code in app.js to render a JSON
object by replacing it with the code below:
The previous code when executed renders an empty JSON object to the browser.
Let’s change this and create a JSON object that we intend to use. Update the code
in app.js to look like the following example:
import http from 'http';
let patches = {
"buzzFunk": [{
"type": "sawtooth",
"frequency": 65.25
}, {
"type": "triangle",
"frequency": 65.25
}, {
"type": "sawtooth",
"frequency": 67.25
}]
}
http.createServer(function (req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(patches)); // non-empty object
}).listen(3000);
console.log("Server listening on port 3000");
The setHeader method also sets the type of content we are sending. In this case,
it is JSON.
res.setHeader('Content-Type', 'application/json');
The res.end method is used to end the response process and, in our case, return a
JSON object to the browser. Before being rendered to the browser the JSON object
must first be converted to a string and this is done using the stringify method.
res.end(JSON.stringify(patches));
You now have working server-side JSON code available for a basic web API. You
will now make a request from a client.
Client Request
On your desktop (or directory of your choice) make a copy of the “web audio
template” folder you created in Chapter 1 and rename the folder “web_app”.
Open the web_app directory in Sublime Text, open the app.js file in the js direc-
tory and type the following code:
fetch("http://localhost:3000/") // points to the endpoint of our
web api server
}, {
"type": "triangle",
"frequency": 65.25,
}, {
"type": "sawtooth",
"frequency": 67.25,
}],
"gameSound": [{ // this is a patch
"type": "square",
"frequency": 100.25,
}, {
"type": "triangle",
"frequency": 65.25,
}, {
"type": "sawtooth",
"frequency": 67.25,
}],
"zapper": [{ // this is a patch
"type": "square",
"frequency": 80.25,
}, {
"type": "triangle",
"frequency": 300.25,
}, {
"type": "sawtooth",
"frequency": 40.25,
},{
"type": "sine",
"frequency": 30.25,
},
{
"type": "sine",
"frequency": 100.25,
}]
}
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>app</title>
<script src="https://ajax.googleapis.com/ajax/libs/
jquery/3.6.0/jquery.min.js"></script>
<script src="js/app.js"></script>
<link rel="stylesheet" href="css/app.css">
</head>
<!--___________________________________________BEGIN APP-->
<body>
<h1>Synthy API </h1>
<h3><span>Click here to turn on synth.</span></h3>
<ul id="piano">
<li>
<div class="white-key key" id="c1"></div>
</li>
<li>
<div class="black-key key" id="c#1"></div>
</li>
<li>
<div class="white-key key" id="d1"></div>
</li>
<li>
<div class="black-key key" id="d#1"></div>
</li>
<li>
<div class="white-key key" id="e1"></div>
</li>
<li>
<div class="white-key key" id="f1"></div>
</li>
<li>
<div class="black-key key" id="f#1"></div>
</li>
<li>
<div class="white-key key" id="g1"></div>
</li>
<li>
<div class="black-key key" id="g#1"></div>
HTML 237
</li>
<li>
<div class="white-key key" id="a1"></div>
</li>
<li>
<div class="black-key key" id="b#1"></div>
</li>
<li>
<div class="white-key key" id="b1"></div>
</li>
<li>
<div class="white-key key" id="c2"></div>
</li>
<li>
<div class="black-key key" id="c#2"></div>
</li>
<li>
<div class="white-key key" id="d2"></div>
</li>
<li>
<div class="black-key key" id="d#2"></div>
</li>
<li>
<div class="white-key key" id="e2"></div>
</li>
<li>
<div class="white-key key" id="f2"></div>
</li>
<li>
<div class="black-key key" id="f#2"></div>
</li>
<li>
<div class="white-key key" id="g2"></div>
</li>
<li>
<div class="black-key key" id="g#2"></div>
</li>
<li>
<div class="white-key key" id="a2"></div>
</li>
<li>
<div class="black-key key" id="b#2"></div>
</li>
<li>
<div class="white-key key" id="b2"></div>
</li>
<li>
<div class="white-key key" id="c3"></div>
</li>
<li>
<div class="black-key key" id="c#3"></div>
</li>
<li>
<div class="white-key key" id="d3"></div>
</li>
<li>
<div class="black-key key" id="d#3"></div>
</li>
<!--____________________________________________END APP-->
</html>
CSS
body{
background-color:purple;
width: 100%;
}
h1{
font-family:"impact";
color:rgb(228, 208, 230);
margin-left:5%;
font-size:70px;
h3{
font-family:"impact";
color:rgb(228, 208, 230);
margin-left:5%;
font-size:30px;
}
li {
list-style:none;
float:left;
CSS 239
display:inline;
width:40px;
position:relative;
}
.white-key{
display:block;
height:220px;
background:#fff;
border:1px solid #ddd;
border-radius:0 0 3px 3px;
}
/* Black keys */
.black-key {
display:inline-block;
position:absolute;
top:0px;
left:-12px;
width:25px;
height:125px;
background:#000;
z-index:1;
Delete any code present in web _ app/js/app.js and replace it with the fol-
lowing code.
"use strict";
let synth;
$(function() {
$(".key").on("mouseover", function() {
$("h3>span").on("click",function(){
synth = apiReader("http://localhost:3000", "buzzFunk"); //
JSON loading function
$(this).text("Synth enabled! Hover over keys to hear sound");
});
});
In your web_app folder, js directory creates a new file named module.js and ref-
erences it in the index.html file between the JQuery library and app.js file.
< head>
<link rel="stylesheet" href="css/app.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/
jquery.min.js"></script>
fetch(endpoint)
.then(response => response.json()) // Convert response
to JSON
.then(data => app.patchParams = data[patchProp]); //
Logs data to console
let app = {
patchParams: undefined,
gainNodes: undefined,
oscillators: undefined,
play: function(id) {
app.oscillators = app.patchParams.map(function(val, i)
{
Launch the index.html file from Sublime Server, in the browser click the text
that says “Click here to turn on synth” and hover your mouse over the synth keys.
You will hear the synth play a collection of oscillators that reference the settings
of the loaded patch data.
$("h3>span").on("click",function(){
synth = apiReader("http://localhost:3000", "zapper");
$(this).text("Synth enabled! Hover over keys to hear
sound");
});
});
In the module.js file, the factory function named apiReader takes two argu-
ments. The first argument is named endpoint and is the endpoint location of
the JSON file. The second argument is named patchProp and is the property
of the JSON object that contains the synth patch data. The endpoint argument is
passed to the fetch function. In the body of the second fetch callback, the selected
patch of the returned object is stored as a property of the app object named
app.patchParams. This is highlighted in the following code example.
fetch(endpoint)
.then(response => response.json()) // Convert response to
JSON
.then(data => app.patchParams = data[patchProp]); //
stores data to app.patchParams
The app object contains the properties and methods that create, connect, start,
and stop oscillators using the selected JSON object data values. The first method
of the app object is named play. It takes a single argument and is the index value
of a DOM element that represents a key. When the play method is invoked,
the map method loops through each object in the app.patchParams array
and creates an oscillator on each iteration. The type, frequency.value, and
detune.value properties of each oscillator are assigned. Each oscillator is then
connected to the node graph and set to start playing.
app.oscillators = app.patchParams.map(function(val) {
let osc = audioContext.createOscillator();
osc.type = val.type;
osc.frequency.value = val.frequency;
osc.detune.value = (val.frequency) + (id * 100);
osc.connect(audioContext.destination);
osc.start(audioContext.currentTime);
return osc;
});
The following code provides the index value of a DOM element (the keyboard note
the user hovers their mouse over) multiplied by 100. The result is added to the
oscillator frequency and assigned to the detune.value property. This makes
the oscillators playback at half-step intervals relative to the keyboard interface.
osc.detune.value = (val.frequency) + (id * 100);
stop: function() {
for (let i = 0; i < app.oscillators.length; i += 1) {
app.oscillators[i].stop(audioContext.currentTime);
}
}
The play and stop methods in web_app/js/module.js are invoked using two
event listeners in web_dev/js/app.js. The event listeners start and stop the oscilla-
tors on mouse events.
$(".key").on("mouseover", function() {
let index = $(this).index('.key');
synth.play(index);
});
$(".key").on("mouseout", function() {
synth.stop();
});
When the play method is invoked, the current index value of the div element
(the “keyboard note”) is captured and passed to the function.
let index = $(this).index('.key');//__get index value of key
synth.play(index);//__________________pass it to play method
{
"buzzFunk": [{
"type": "sawtooth",
"frequency": 65.25,
"volume": 1
}, {
"type": "triangle",
"frequency": 65.25,
"volume": 1
}, {
"type": "sawtooth",
"frequency": 67.25,
"volume": 0.3
}]
}
fetch(endpoint)
.then(response => response.json()) // Convert response
to JSON
.then(data => app.patchParams = data[patchProp]); //
Logs data to console
let app = {
patchParams: undefined,
gainNodes: undefined,
oscillators: undefined,
play: function(id) {
app.gainNodes = app.patchParams.map(function(val) {
let gain = audioContext.createGain();
gain.gain.value = val.volume;
return gain;
});
app.oscillators = app.patchParams.map(function(val, i)
{
return osc;
});
},
stop: function() {
for (let i = 0; i < app.oscillators.length; i += 1) {
app.oscillators[i].stop(audioContext.currentTime);
}
}
};
return app;
};
These file modifications give your code the ability to create a gain node for
each oscillator. The play method of the app object contains a map method
that creates the gain nodes and sets the gain.gain.value property for
each one. All gain nodes are placed in an array that is assigned to app.
gainNodes.
The oscillators are then connected to the gain nodes in the second map method.
app.oscillators = app.patchParams.map(function(val, i) {
let osc = audioContext.createOscillator();
osc.type = val.type;
osc.frequency.value = val.frequency;
osc.detune.value = (val.frequency) + (id * 100);
osc.connect(app.gainNodes[i]);
app.gainNodes[i].connect(audioContext.destination);
osc.start(audioContext.currentTime);
return osc;
});
Summary
In this chapter, you learned how to query third-party web APIs, work with JSON
files, and create your own web API to load patch data for a synthesizer. The appli-
cation you created only begins to explore what is possible. For a challenge, try
incorporating filters, LFOs, delays, and other settings. For another challenge,
create an HTML form that lets users load patches from the applications UI
directly. In the next chapter, you will learn about the future of JavaScript and
various resources for continued learning.
Summary 245
25 The Future
of JavaScript
and the Web
Audio API
In this book, you have learned the core concepts behind the JavaScript program-
ming language and the Web Audio API. To keep from overcomplicating things,
parts of both the JavaScript language and the Web Audio API have been omitted.
This chapter presents some of the areas that were skipped and provides you a few
suggestions about what you can learn now to future-proof your new skills.
Node.js
https://nodejs.org
Node.js is a server-side JavaScript environment based on V8, which is the
same JavaScript engine that runs Google Chrome. Instead of running JavaScript
from a web browser, Node.js allows you to run JavaScript from the terminal on
your computer. It can be used to automate computer tasks, run web servers, and
communicate with databases.
Summary
In this chapter, you were presented with a list of options for continued learn-
ing. Even though JavaScript has been taught here in the context of working with
audio, it is important to keep in mind that programming is a useful cross-disci-
plinary skill that you can use to solve many different types of problems.
Further Reading
◾ JavaScript: The Definitive Guide by David Flanagan.
Book Website
http://javascriptforsoundartists.com
251
audio parameters (cont.) Code Snippets create 6–7
over time 193–194 comments 14
setTargetAtTime() method 195 comparison operators 30
setValueAtTime method 194 concat() 25
setValueCurveAtTime() method concatenation 16
195–196 conditional statements 35
audio visualizations if 36–37
binary-coded decimal numbers 198 switch 37–38
CSS 200–201 ternary 38–39
display interface 203–204 console.log() 15–16
Document Object Model 204–205 const 13
Fourier analysis 197–198 constructors 135
frequency data 202 adding methods to 140
frequencyData array 203 exist 141
HTML 199–200 and new keyword 139–140
JavaScript/JQuery 199 prototype object 140–141
spectrum analyzer 198 prototype property 140–141
walking through 201–202 Web Audio API 141
content delivery network (CDN) 115–116
B convolver node
base case 63 convolution reverb 175–176
BigInt 23 get pre-recorded impulse response
bind function 72–74 files 176
biquad filter node 167–168 HTML 177
equalizer create 170 JavaScript 177
filter types 168–170 reverberation control 178
graphic EQ 170–172 using impulse response files 176–177
parametric EQ 172–173 CSS 2
block scoped variables 42–43 audio visualizations 200–201
Boolean data type 30 sequencers 221–223
Brave Browser 7 user interface 87–89
browser 6
buzzFunk 243 D
delay node 183
C echo effects 184
calculateFrequencies 59 ping-pong effect 185–186
callback function 59–60 slap back effect 184–185
camel case 11 descendent selectors 90–91
centering block-level elements 98–101 destination 10
channel merger 181 detune property 79–80
channel splitter 180–181 Developer Tools 7
charAt() 18 division assignment 29
child selectors 91 Document Object Model (DOM)
class 91, 135 audio visualizations 204–205
concept 136 building application 105
example 143–144 frequency changes 109–110
in JavaScript 142–143 frequency slider programme 108–109
private data and 144–145 HTML 103
cloning objects 69 JavaScript 103, 104
closures 56–59 toggling start/stop text 106–108
252 Index
Trigger an Oscillator by Clicking getters 138–139
Button 105–106 globally scope 42
waveforms changes 111–112 greater than operators 31–32
waveform selection 112–114 grouping selectors 90
dynamic object extension 137–138
dynamic range compression 187–189 H
Hello Sound Program 9–11
E “Hello World” 9
echo effects 184 higher scope access 43
effectsBox function 57 hoisting
element selectors 90 and functions 54–55
equality operators 30–34 with let and const 45
“equal sign” 12 and variables 44
exponentialRampToValueAtTime method hypertext markup language (HTML) 2
194–195 audio visualizations 199–200
convolver node 177
F Document Object Model 103
factory 135 sequencers 221
pattern 136–137 user interface 81–86
Fetch API 128
fileDirectory 150 I
filter() 60 id 91
font size, style (type), and color 97–98 immediately invoked function expression
form elements 86–87 (IIFE) 55
frequencyData array 203 innerFunction() 57
frequency property 79 input elements 86–87
functions
abstracting oscillator playback 50 J
anonymous 55–56 JavaScript 1, 3
arguments object 51–52 audio visualizations 199
arrow syntax 61–62 class 142–143
callback 59–60 convolver node 177
closures 56–59 data types 65–67
default arguments 51 Document Object Model 103, 104
expressions 49 JQuery 120
filter() 60 Web Audio API 248
hoisting and 54–55 JQuery 115
map() 61 with 121
parts of 48–49 audio visualizations 199
recursion 63 from CDN 116–117
rest parameter 52 DOM selectors 117
scope 52–53 event listener 122–123
simple example 47–48 HTML elements select 117
variables 54 JavaScript 120
working effects box example 50–51 method chaining 119
onOff Method 124–125
G oscillator player refactoring 120–121
gain nodes 156–157 reference directly 116
getComponent 58 setInterval 123–124
get requests 130 setup 115–116
Index 253
JQuery (cont.) multiplication assignment 29
this keyword 120 multiplier 53, 54
user-selected list element 122
using methods 118 N
without 121 Nested Object Gotcha 70–71
JSON 228 Netscape 1
node graphs 76
L effects 157–158, 159
length property 19 gain nodes 156–157
less than operators 31, 32 placement 157
linearRampToValueAtTime method 195 real-world example 159
list-style-type 96 think about 155–156
LiveScript 1 Node.js 230–231, 248
loading audio file non-block scoped variables 43
audio buffer 132–133 not equal to operator 32
compatibility 130 NOT Operator 33–34
get requests 130 null 14
playing 132 numbers 19–20
steps 128 to string conversion 22–23
synchronous vs. asynchronous
code execution 130–132 O
using Fetch API 133–134 Object.create 70–71
with XMLHttpRequest object objects
128–130 access 68–69
local scope access 43 arrays 68
logical AND operator 33 bind function 72–74
logical operators 32–33 cloning 69
logical OR Operator 33 looping through 67–68
loopFromTo 63 Object.create 70–71
looping sounds 192–193 prototypal inheritance 70
loops this keyword 72
for 39–41 onended property 77
while 41–42 onOff Method 124–125
lowpass 159 openEffectsBox function 57–58
Open Sound Control (OSC) 249
M operators 27–28
map() 61 assignment 28–29
Math.abs() 22 Boolean data type 30
Math.ceil() 21–22 comparison 30
Math.floor() 21–22 equality 30–34
Math.max() 21 oscillators 76–77, 78
Math.min() 21
Math.random() 22 P
method chaining 119 ping-pong effect 185–186
MIDI API 248 pop() 24
module.js 244–245 prerequisites 127–128
Modulo Assignment 29 primitive data types 65
multFreq 53 private data 138
multichannel file, into single mono program, defined as 1
channel 181–182 prototypal inheritance 70
254 Index
prototype object 140–141 timing clock 191–192
prototype property 140–141 toLowerCase() 17–18
push() 24 toUpperCase() 17
troubleshooting problems 7
R type property 78–79
recursion 63
reference JQuery directly 116 U
replace() 18 undefined keyword 13
unshift() 25
S user interface (UI) 81
scope 42–43 App Interface modification 91–95
functions 52–53 bullet points remove 96–97
sequencers 207 centering block-level elements 98–101
building 213–215 child selectors 91
changing tempo 212–213 class and id 91
CSS 221–223 comments 90
HTML 221 CSS 87–89
interactivity 223–224 descendent selectors 90–91
playing back sounds 215–220 element selectors 90
problem 207–208 font size, style (type), and color 97–98
setInterval/setTimeout 208–209 form and input elements 86–87
setTimeout 209–212 grouping selectors 90
solution 209 HTML 81–86
user interface grid 220 margin, border, and padding 95–96
setInterval() 109
setInterval 123–124, 208–209 V
setTargetAtTime() method 195 variables 11–13
setters 138–139 with an oscillator 14–15
setTimeout 208–212 block scoped 42–43
setValueAtTime method 194 data types 20
setValueCurveAtTime() method 195–196 DOM selectors 117
shift() 25 hoisting and 44
slap back effect 184–185 non-block scoped 43
slice() 19
soundBuffer 151 W
start method 192 web application 3
stereo panner node 179–180 Web Audio API 3–4, 75, 141, 247
channel merger 181 audio buffer data 248
merger and splitter nodes 182 JavaScript 248
multichannel file, into single mono libraries and front-end
channel 181–182 frameworks 248
stop method 77 Node.js 248
strict equality operator 31 Web Audio 3D spacial positioning 247
strict not equal to operator 32 with JQuery 121
string 12, 16–19 without JQuery 121
subtraction assignment 29 work environment 4–6
T X
testScope() 57 XMLHttpRequest 128–130
this keyword 72, 120
Index 255
Taylor & Francis eBooks
www.taylorfrancis.com
Improved
A streamlined A single point search and
experience for of discovery discovery of
our library for all of our content at both
customers eBook content book and
chapter level