Building a Drum Sequencer

I’m a hobbyist electronic musician.   I’ve always loved experimenting with hardware, synthesizers, drum machines, and midi controllers. Naturally,  I wanted to create a fun, creative, web based sequencer that might serve as a gateway for others into developing a love for creating electronic music.

Last week, a coworker and friend challenged me to create a drum machine. The grand goal being to build an online DAW for people to collaborate on as a community.  That goal is a bit far off yet, but what isn’t is getting started right away exploring what can be done using web based music making tools people can play with.

I took that a step further beyond the usual MPC style drum machine that you will find across the internet, due to the challenge being officially part of the project curriculum of freecodecamp.org  These drum machines are usable if you have impeccable timing and dexterous fingers. After a few novel minutes, they tend to lose my interest, and probably yours too.  I wanted more than simply playing a sound on event.keyCode

I’m a hobbyist electronic musician.   I’ve always loved experimenting with hardware synthesizers, drum machines, and midi controllers. Naturally,  I wanted to create a fun, creative, web based sequencer that might serve as a gateway for others into developing a love for creating electronic music.

Ideas Begin to Take Shape

I had a basic idea of what I wanted, but not much else.  I had experience using sequencers to make music, but I had no idea how this sort of thing was implemented.  I gestated on this for a few days and all of a sudden, the implementation came to me in the shower.

Yes in the shower, as cliche as that is.  The mind is a funny thing and sometimes works on the problem for you in the background processes of your brain.  

In a flurry, I took to my white board before I was even fully dry from the shower and started scribbling out what had been bestowed upon my conscious mind so suddenly.

 

After scratching down my preliminary ideas, I opened VS code and got down to coding it while the ideas were loaded into my head and ready to fly out of my fingers.  Little did I know I was about to fall into a rabbit hole and entire universe of web audio, and the limitations of Javascripts single thread model.

I’ll try and lay out my initial thought process as it slowly occured to me in the eureka moment.

  1. sequencer needs ability to play samples that overlap on the same tick (note)
  2. sequencer engine needs to loop at any length and tempo set by user 
  3. volume needs to be independently adjustable per sound
  4. UI should be dynamically adjustable to the amount of bars set by user
  5. UI should have full CRUD functionality in order to be useful.

Sequencer Needs Ability to Play Samples That Overlap

What data structure needed to be used?  It seemed obvious I needed to use an array list of objects.

The naive approach would be to set a string representing the sample to be played and use a hash map to find the sample and play it.   If I did this than for each sample, I would have to have one loop running for each sample used.  That seemed like a lot more orchestration than I was willing to keep track of.   Instead, I chose to use a factory to create the object that gets pushed to each index of an array that represents the sequence.  As the play head or needle passed over the index, it would check the properties of this object and read if the sound was set to true or false.  If it was set to true, it would then play that sound.

Below is my first attempt at sketching out something that would prove to myself that I was on the right track .  The first stab at this was done in node and not the browser.

let player = require("play-sound")();

// For each tick in a sequence this object will be checked
// to determine what instruments need
// to be triggered for the current tick.

function DrumMachineState(id) {
  this.id = id;
  this.kick = {
    on: false,
    name: "kick",
    location: "./samples/Deep House Drum Samples/bd_kick/bd_deephouser.wav"
  };
  this.clap = {
    on: false,
    name: "clap",
    location: "./samples/Deep House Drum Samples/clap/clp_analogue.wav"
  };
  this.hat = {
    on: false,
    name: "hat",
    location: "./samples/Deep House Drum Samples/hats/hat_analog.wav"
  };
  this.shaker = {
    on: false,
    name: "shaker",
    location:
      "./samples/Deep House Drum Samples/shaker_tambourine/shaker_bot.wav"
  };
  this.perc = {
    on: false,
    name: "perc",
    location: "./samples/Deep House Drum Samples/percussion/prc_bongodrm.wav"
  };
  this.perc2 = {
    on: false,
    name: "perc2",
    location: "./samples/Deep House Drum Samples/percussion/prc_congaz.wav"
  };
}

DrumMachineState.prototype.getState = function(name) {
  return this[name];
};
DrumMachineState.prototype.setState = function(name, on, volume) {
  this[name].on = on;
  this[name].volume = volume || 1;
  this[name].name = name;
};

// dependency inject the drum machine state into sequencer
function Sequencer(drumMachineState, length) {
  this.drumMachineState = drumMachineState;
  this.length = length;
  this.sequence = [];
}

Sequencer.prototype.initSeq = function() {
  for (let i = 0; i < this.length; i += 1) {
    this.sequence.push(new this.drumMachineState(i + 1));
  }

  return this.sequence;
};

link to the commit shown above

The DrumMachineState constructor function is the meat of the sequencer engine.  It serves as a sort of factory for creating objects with the properties checked on each tick as the playhead passes over each note in the sequence.

I then used another constructor function to dependency inject the DrumMachineState as a property of the sequence.  I then initialized the sequence by instantiating a DrumMachineState object into each index of the sequence array thus creating a complete playable sequence to loop over.  I think this was a much better solution than trying to sync up multiple loops for each drum sound.

First requirement was complete!  Now how do I get this darn thing to actually play something and loop?  setInterval!

It wasn’t the best solution in hind sight due to setInterval being pretty inaccurate.  Some of you JS audio programming geeks know the pitfalls of setInterval sharing the single thread javascript runs on but gimme a break it’s my first try at a project like this.  We get into this in part 2 and explain why my approach will be different on version 2.  

Sequencer Engine Needs to Loop at Any Length and Tempo Set by User

Enter the setInterval loop.  Good old setInterval – the trusty tool for long polling data from REST endpoints to make it look like an open websocket!   I had a much more fun plan for setInterval this time.   It was going to be the heartbeat of the sequencer.  I had a few problems to solve first though.

  1. How would I set the interval according to a beats per minute unit of time?
  2. On each tick how do you actually read all the drum hits that are set to boolean of true and then play a selected wave file?
  3. How do we ensure that it loops around no matter what the length of the sequence is?

I chose to use a plain old singleton object because we only ever needed one of these.  This serves as the transport commonly referred to by Digital Audio Workstations.

How Would I Set the Interval According to a Beats Per Minute Unit of Time?

setInterval accepts as its second parameter an argument in milliseconds.  Instead of making the user of the method guess at how many milliseconds 120 bpm is, I created a method to make the conversion based on a tempo and a ticks per beat. Ticks per beat  selects the resolution of your smallest note value.

There are 60,000 milliseconds in a minute so if you want to know how long a beat is in milliseconds for any tempo, then follow this formula:

60,000 / BPM = one beat in milliseconds

setTempo: function(tempo, ticksPerBeat = 1) {
    let ms;
    switch (ticksPerBeat) {
      case 1:
        ms = 60000 / tempo;
        break;
      case 2:
        ms = 30000 / tempo;
        break;
      case 4:
        ms = 15000 / tempo;
      default:
        ms = 15000 / tempo;
    }
    this.tempo = ms;
  },

drum machine file at the first commit for context

Now I could use the ms conversion in the start method like so.  start() sets up the Interval at the correct tempo and loads it into a property called playingSeq so that it can clear the interval later.  It then passes triggerSounds() the sequence array, grabs the index based on where the playHead is which  is then consumed by this method, and increments the play head after triggering sounds.  It then does a check to see if we have reached the end of the loop.  If so it reset the play head to zero and on and on looping we go until we decide we have had enough and stop the sequence.

start: function start(initializedSequence) {
    this.playingSeq = setInterval(() => {
      this.triggerSounds(initializedSequence[this.playHead]);
      this.playHead++;
      if (this.playHead === initializedSequence.length) {
        this.playHead = 0;
      }
    }, this.tempo);
  },

Stopping is a pretty simple affair of clearing the interval.  The funny biz with the m parameter is just me playing around with delaying the stop and starting another one after returning a promise. If you look into the commits you will see that in the original node based sequencer.  The only thing that is important is that of clearing the interval using the reference to the previously set interval.

stop: function stop(m) {
    if (m === undefined) {
      clearInterval(this.playingSeq);
    }
    setTimeout(() => {
      clearInterval(this.playingSeq);
    }, m);
  },

Now we come to the method that actually does the work of playing the sounds.  This is where the rubber meets the road and I finally got the sequencer to loop over sounds!

This method takes in the current drum state object at current index (note) and reaches into it and finds any drums that are set to on=true and plays them at the right volumes.

The player I’m using is a node npm package called play-sound

triggerSounds: function triggerSounds(dmState) {
    for (let instrument in dmState) {
      if (dmState.hasOwnProperty(instrument)) {
        if (dmState[instrument].on) {
          console.log(dmState.getState(instrument).location);
          player.play(dmState.getState(instrument).location, {
            afplay: ["-v", dmState.getState(instrument).volume]
          });
        }
      }
    }
  }

Finally I use the setter and getter methods I put on the prototype of every drum state object to build the sequence without a UI.

const sequencer = new Sequencer(DrumMachineState, 8);
const seq = sequencer.initSeq();

seq[0].setState("kick", true, 1);
seq[4].setState("kick", true, 0.8);
seq[4].setState("clap", true);
seq[2].setState("hat", true, 0.2);
seq[6].setState("hat", true, 0.24);
seq[0].setState("shaker", true, 0.2);
seq[1].setState("shaker", true, 0.1);
seq[2].setState("shaker", true, 0.2);
seq[3].setState("shaker", true, 0.1);
seq[4].setState("shaker", true, 0.2);
seq[5].setState("shaker", true, 0.1);
seq[6].setState("shaker", true, 0.2);
seq[7].setState("shaker", true, 0.1);
seq[1].setState("perc", true, 0.3);
seq[4].setState("perc", true, 0.4);
seq[5].setState("perc", true, 0.3);
seq[2].setState("perc2", true, 0.3);
seq[3].setState("perc2", true, 0.5);
seq[7].setState("perc2", true, 0.25);

dm.setTempo(127, 4);
dm.start(seq);

and BOOM!! IT’S ALIVE!!!!!!!!!!!!

 

70bf5f017837cd89b7dbcfeb1a0c8d6e8d2d72d931e63a762a6aac0bd12f7498

Stay tuned for Part 2 where I move from a rough draft in node to the front end. I then  build out all functionality in Vanilla.js.

to be continued…..

TL;DR? Drum sequencer github project

What to do when you fail an algorithm challenge

Yesterday I woke up bright and early. I poured a strong cup of joe and set off to http://www.codewars.com to train with a new code Kata.  I was filling confident after solving one the day before so I thought why not up the ante a bit and try a 5 kyu challenge called “Weight for weight”. In the end I got a wallop of a black eye. Fortunately, we all do our best learning from struggle.

Yesterday I woke up bright and early. I poured a strong cup of joe and set off to http://www.codewars.com to train with a new code Kata.  I was feeling confident after solving one the day before so I thought why not up the ante a bit and try a 5 kyu challenge called “Weight for weight.” In the end, I got a wallop of a black eye. Fortunately though, we all do our best learning from struggle.

I hacked away at it for hours I must admit and I almost solved it, but in the end I caved under the weight of it all (pun intended).  Let’s see the instructions for the challenge and then I’ll show you my ham-handed attempt at solving it.  After that I’ll show a solution I found on github and how I went about analyzing it with the chrome debugger.

Weight for weight 5 kyu challenge

Instructions:

My friend John and I are members of the “Fat to Fit Club (FFC)”. John is worried because each month a list with the weights of members is published and each month he is the last on the list which means he is the heaviest.

I am the one who establishes the list so I told him: “Don’t worry any more, I will modify the order of the list”. It was decided to attribute a “weight” to numbers. The weight of a number will be from now on the sum of its digits.

For example 99 will have “weight” 18, 100 will have “weight” 1 so in the list 100 will come before 99. Given a string with the weights of FFC members in normal order can you give this string ordered by “weights” of these numbers?

Example:

“56 65 74 100 99 68 86 180 90” ordered by numbers weights becomes: “100 180 90 56 65 74 68 86 99”

When two numbers have the same “weight”, let us class them as if they were strings and not numbers: 100 is before 180 because its “weight” (1) is less than the one of 180 (9) and 180 is before 90 since, having the same “weight” (9) it comes before as a string.

All numbers in the list are positive numbers and the list can be empty.

Here is my almost-working solution.  My solution is not very well composed and doesn’t even address the last bit of instruction listed below:


function orderWeight(strng) {
  var strArr = strng.split(' ');
  var newArr = [];
  var sum = 0;
  var numToAdd = 0;
  var obj = {};
  var orderedArr = [];

  function convertToObj(strArr, obj) {
    strArr.forEach(function(str, index) {
      for (var i = 0; i < str.length; i++) {
        numToAdd = parseInt(str.charAt(i), 10);
        sum += numToAdd;
      }

      obj[str] = sum;
      console.log(obj[str]);
    });
    return obj;
  }
  convertToObj(strArr, obj);

  console.log(obj);

  for (var prop in obj) {
    console.log(prop.toString());

    newArr.push([prop, obj[prop]]);
  }

  newArr = newArr.sort(function(a, b) {
    return a[1] + b[1];
  });

  for (var j = 0; j < newArr.length; j++) {
    orderedArr.push(newArr[j][0]);
  }

  var finalStr = orderedArr.join(" ");
  console.log(finalStr);
}

When two numbers have the same “weight”, let us class them as if they were strings and not numbers: 100 is before 180 because its “weight” (1) is less than the one of 180 (9) and 180 is before 90 since, having the same “weight” (9) it comes before as a string.

My output: 180 100 99 90 86 74 68 65 56
correct output: "100 180 90 56 65 74 68 86 99"

I ended up chasing my own tail like a hyper little dog. You don’t need to concentrate at all on my non-solution, I just want to show you for illustrative purposes. My thinking was to make a simple hash with the prop being the actual number and the prop value being the sum of counting each individual place, so if the num was 235 then the resulting hash would be:

2 + 3 + 5 = 10 {235:10}

The problem with using that method was trying to figure out how to then output the prop name which was the real number in the order I wanted, while also ordering the numbers based on string sort. The evidence of my tail chasing is above. This is not to say I didn’t learn anything. I learned quite a lot about how to manipulate objects from this challenge. At the end of this post I’ll share all the research from my head scratching.

Now let’s see an elegant solution. I ran it through the debugger to understand how it works.  Credit goes to http://www.paigebolduc.com/  for the featured solution.


function orderWeightV2(strng) {
  var weights = strng.split(' '); // splits string into an array based on
spaces

// divide and conquer
// sort takes 2 array index's at a time (a, b) and allows you to do a
comparison between each pair
  weights.sort(function(a, b) {
    var aSum = getSum(a); // call getSum() ie: value 246 = 2 + 4 + 6 = 12
    var bSum = getSum(b); // call it again with b 

    if (aSum === bSum) { // compare based on sum only
      if (a < b) {
        return -1; // -1 means sort a before b
      } else {
        return 1; // 1 means sort b before a
      }

    } else if (aSum < bSum) { if a is less that b keep it same
      return -1;
    }
    return 1;  // otherwise b before a
  });

  return weights.join(" ");
}

// called above inside sort
function getSum(str) {
  return str.split('').reduce(function(sum, next) {
    return sum + Number(next);
  }, 0);
}

// the above function uses reduce but could also be written like below
function getTotal(str) {
  str = str.split('');
  var sum = 0;
  for (var i = 0; i < str.length; i++) {
    sum += parseInt(str[i]);
  }
  return sum;
}

I opened the console by hitting Ctrl+Shift J, then moved over to the sources tab and stepped through the solution by hitting F11 (you can see with the featured animation at the top of the post).

My problem was not fully understanding the MDN explanations of sort method.  I found this article called, “Sophisticated Sorting” which made more sense.  It goes into much more versatile ways of using sort().

So I failed the challenge, but in the struggle I learned more about objects and the sort method. I feel like I was able to turn it into a win and that is what practicing algorithms is about. I’ve heard quite a few complaints from Free Code Campers that the algorithms are too hard. They aren’t too hard and your supposed to fall on your face before being able to solve them.

Do not think of them as goals to get past. Approach them as an opportunity to explore new concepts. As with many things in life it’s about the journey and not the end goal sometimes. Of course you want to solve them in the end, but never at the expense of the struggle of finding the solution.

Beyond everything else, remember that you’re not the stupid one. It’s the computer that is dumb as a rock. You just need to learn to give the computer more explicit instructions. 😀

research links from trying to solve this challenge:

Practicing with data (map, filter, loops, objects, arrays)

I thought I would mock up some fun “Star Wars” data and riff on it.

Lets code with the force!

I thought I would mock up some fun “Star Wars” data and riff on it. I’m just gonna play around see what I can come up with using some tools I need to become more familiar with myself. Never underestimate the power of learning through play! Who knows maybe someone else will learn along with me. If you’re at an advanced level this post will not be for you.


var starWarsCharacters = [{
  name: "Darth Vader",
  role: "Evil Sith Lord",
  morals: "evil",
  team: "Galactic Empire",
  episodes: [3, 4, 5, 6],
  relationships: ["husband of Padme",
    "father to Luke Skywalker", "formally known as Anakin Skywalker",
    "Student to Obi Wan"
  ]
}, {
  name: "Luke Skywalker",
  role: "Jedi",
  morals: "good",
  team: "Rebel Alliance",
  episodes: [4, 5, 6, 7],
  relationships: ["Single", "brother of Lea", "son of Darth Vader"]
}, {
  name: "Lea",
  role: "rebel princess",
  morals: "good",
  team: "Rebel Alliance",
  episodes: [4, 5, 6, 7],
  relationships: ["dating Han Solo", "daughter of Darth Vader",
    "sister to Luke Skywalker"
  ]
}, {
  name: "Han Solo",
  role: "smuggler",
  morals: "good",
  team: "Rebel Alliance",
  episodes: [4, 5, 6, 7],
  relationships: ["dating Lea", "Chewbacca's best friend"]
}, {
  name: "Chewbacca",
  role: "smuggler",
  morals: "good",
  team: "Rebel Alliance",
  episodes: [4, 5, 6, 7],
  relationships: ["Han Solo's best friend"]
}, {
  name: "Darth Sidious",
  role: "emperor",
  morals: "evil",
  team: "Galactic Empire",
  episodes: [1, 2, 3, 4, 5, 6],
  relationships: ["Anakin Skywalker's evil mentor", "Darth Maul's Master"]
}, {
  name: "Kylo Ren",
  role: "Leader of the First Order",
  morals: "evil",
  team: "First Order",
  episodes: [7],
  relationships: ["Grand son of Darth Vader",
    "son of Han Solo and Lea Skywalker"
  ]
}, {
  name: "Count Dooku",
  role: "Separatist Leader",
  morals: "evil",
  team: "Separatists",
  episodes: [2, 3],
  relationships: ["Trained Qui Gon Jinn"]
}, {
  name: "Qui Gon Jinn",
  role: "Jedi",
  morals: "good",
  team: "Old Republic",
  episodes: [1],
  relationships: ["Count Dooku's student", "Obi Wan's master"]
}, {
  name: "Obi Wan",
  role: "Jedi",
  morals: "good",
  team: "Old Republic",
  episodes: [1, 2, 3, 4],
  relationships: ["Qui Gon Jinn's student", "Anakin Skywalker's master"]
}, {
  name: "Lando Calrissian",
  role: "Businessman & Scoundrel",
  morals: "good",
  team: "Rebal Alliance",
  episodes: [5, 6],
  relationships: ["Han Solo's friend"]
}, {
  name: "Darth Maul",
  role: "Sith",
  morals: "evil",
  team: "Galactic Empire",
  episodes: [1],
  relationships: ["Darth Sidious's student"]
}];

One thing I remember scratching my head on as an absolute beginner was accessing arrays within objects within arrays.  It gets easier as you practice though and before you know it, it’s second nature!

So first before we start using filter and map and all that fancy stuff lets access it in the most basic way possible.


for (var i = 0; i < starWarsCharacters.length; i++) {
  console.log(starWarsCharacters[i].name);
}

// this prints out all of the names of course

main.js:98 Darth Vader
main.js:98 Luke Skywalker
main.js:98 Lea
main.js:98 Han Solo
main.js:98 Chewbacca
main.js:98 Darth Sidious
main.js:98 Kylo Ren
main.js:98 Count Dooku
main.js:98 Qui Gon Jinn
main.js:98 Obi Wan
main.js:98 Lando Calrissian
main.js:98 Darth Maul

What if we wanted to only print out the names of good guys?


for (var i = 0; i < starWarsCharacters.length; i++) {
  if (starWarsCharacters[i].morals !== "evil") {
    console.log(starWarsCharacters[i].name);
  }
}

// this prints out all the good guys and gals!

main.js:103 Luke Skywalker
main.js:103 Lea
main.js:103 Han Solo
main.js:103 Chewbacca
main.js:103 Qui Gon Jinn
main.js:103 Obi Wan
main.js:103 Lando Calrissian

Now lets do the same thing in a less ugly way using the filter method.

var goodguys = starWarsCharacters.filter(function(character) {
  return character.morals !== "evil";
});
console.log(goodguys);

// output is an array of objects
main.js:110 [Object, Object, Object, Object, Object, Object, Object]

Now if we go back and change the previous vanilla for loop we can do the same thing.


var arr = [];
for (var i = 0; i < starWarsCharacters.length; i++) {
    if (starWarsCharacters[i].morals !== "evil") {
        arr.push(starWarsCharacters[i]);
}
}

// exactly the same output with way more typing

main.js:120 [Object, Object, Object, Object, Object, Object, Object]

Lets build a simple filter function to do the same thing again.  (You don’t have to use this ever again.) Use the one that is already attached to Array.prototype out of the box.  It’s only being shown so that you can easily understand what is going on behind the scenes.  The Polyfill is much more complicated.


function filterIt(arr, test) {
  var passTest = [];
  for (var i = 0; i < arr.length; i++) {
    if (test(arr[i])) {
      passTest.push(arr[i]);
    }
  }
  return passTest;
}

console.log(filterIt(starWarsCharacters, function(character) {
  return character.morals !== "evil";
}));

// again same output of objects

Next how about we build a function to see which team each character is on.


var team = function(alliance) {
  var alliances = starWarsCharacters.filter(function(char) {
    return char.team.toLowerCase() === alliance.toLowerCase();
  });
  // map is used here to build an entirely new returned object of only the relevant data
  alliances = alliances.map(function(char) {
    return {
      name: char.name,
      team: char.team
    };
  });
  return alliances;
};

console.log(JSON.stringify(team("rebel AlLiance"), null, 4));

[
    {
        "name": "Luke Skywalker",
        "team": "Rebel Alliance"
    },
    {
        "name": "Lea",
        "team": "Rebel Alliance"
    },
    {
        "name": "Han Solo",
        "team": "Rebel Alliance"
    },
    {
        "name": "Chewbacca",
        "team": "Rebel Alliance"
    }
]


Next we will write a function that will give us all the characters in a selected episode we select as an argument.


var charactersInEpisode = function(episode) {
  var chars = [];
  starWarsCharacters.forEach(function(char) {
    char.episodes.forEach(function(ep) {
      if (ep === episode) {
        chars.push(char.name);
      }
    });
  });
  return chars;
};

console.log(JSON.stringify(charactersInEpisode(2), null, 4));

[
    "Darth Sidious",
    "Count Dooku",
    "Obi Wan"
]

How about a function that finds related characters?


var related = function(char) {
  var chars = [];
  starWarsCharacters.filter(function(character) {
    return character.relationships.filter(function(relationship) {
      if (relationship.toLowerCase().match(char.toLowerCase())) {
        chars.push(character.name);
        return;
      }
    });
  });
  return chars;
};

console.log(JSON.stringify(related("DaRTh VaDeR"), null, 4));

[
    "Luke Skywalker",
    "Lea",
    "Kylo Ren"
]

How about a function to win star wars by rejecting all the bad guys and while we are at it let’s show how many episodes they were in.


var winStarWars = starWarsCharacters.filter(function(char) {
  return char.morals !== 'evil';
});

var goodchars = winStarWars.map(function(goodchar) {
  // transform data into a new object
  return {
    name: goodchar.name,
    episodesIn: goodchar.episodes.length,
    role: goodchar.role
  };
});

console.log(JSON.stringify(goodchars, null, 4));

[
    {
        "name": "Luke Skywalker",
        "episodesIn": 4,
        "role": "Jedi"
    },
    {
        "name": "Lea",
        "episodesIn": 4,
        "role": "rebel princess"
    },
    {
        "name": "Han Solo",
        "episodesIn": 4,
        "role": "smuggler"
    },
    {
        "name": "Chewbacca",
        "episodesIn": 4,
        "role": "smuggler"
    },
    {
        "name": "Qui Gon Jinn",
        "episodesIn": 1,
        "role": "Jedi"
    },
    {
        "name": "Obi Wan",
        "episodesIn": 4,
        "role": "Jedi"
    },
    {
        "name": "Lando Calrissian",
        "episodesIn": 2,
        "role": "Businessman & Scoundrel"
    }
]

That is enough for now! Below is a resource I found informative and useful in learning some of these concepts.

joepie91’s Ramblings