Blog

  • Creating A BPM Finder Using the Spotify API

    Creating A BPM Finder Using the Spotify API

    You can find the finished bpm finder here.

    This post details the process of creating a BPM (Beats Per Minute) finder using the Spotify API and JavaScript. By inputting a song name into the search bar, the tool provides the top result and gives the estimated BPM. It’s a tool useful for musicians, producers or DJs, which saves time in finding the tempo of any song they are working with.

    Setting up API access

    Spotify provides a public REST API that gives information about their music library, such as track information and song features. By authenticating an individual Spotify account it’s also possible to manage playlists and control account playback.

    To access it, first I created an app on the Spotify developer dashboard.

    Once created, Spotify provides a ‘client ID’ and ‘client secret’ to allow access to their library. Their documentation provides a detailed step by step guide to setting it up.

    For this project, as I’m accessing publicly available data, I have chosen to use the ‘client credentials’ authentication flow as this flow allows the end user to use the tool without authenticating their Spotify account.

    Testing the API in Postman

    To check that the API is working correctly with the credentials I’ve been given, I performed some test API requests in Postman. Postman is a free API platform that allows you to make API requests. I used this tool to determine how the response data was structured.

    Choosing the end points

    The Spotify API has a number of endpoints which allow you to retrieve different information. For this project, I’ve chosen the following ones:

    Retrieve Access Token

    Endpoint: https://accounts.spotify.com/api/token

    This is used to retrieve an access token to authorize requests. The access token can be obtained by sending the client secret and client ID provided to this endpoint.

    Search for Item

    Endpoint: https://api.spotify.com/v1/search

    By sending a query string, along with the access token, to this endpoint we can search for tracks in the Spotify database. This will be used to retrieve the song ID of the top search result.

    Get Track’s Audio Features

    Endpoint: https://api.spotify.com/v1/audio-features/{id}

    By adding a Spotify song ID to the endpoint URL, we can retrieve information about the song such as danceability, loudness or tempo. This will be used to retrieve the estimated song BPM.

    Creating The Website

    To create the BPM finder, I’ve embedded the code into my WordPress site. The page is written with a combination of HTML, Javascript and CSS.

    HTML

    On a blank webpage, inside a code snippet, I created a div to insert all the elements of the finder. I used Meta’s Imagine Image Generator to create an piece for the site, based on the theme ‘bpm finder’. After a few iterations, I settled on the below image:

    The HTML structure:

    <div id="container">
      <img
        src="https://seanprailton.com/wp-content/uploads/2024/01/BPM-finder_-line-art-removebg-preview.png"
        alt=""
      />
      <h1>bpm finder</h1>
      <div class="name">By Sean Railton</div>
      <div class="spacer"></div>
      <div class="search-container" id="search-container">
        <input type="text" id="songSearch" placeholder="Enter a song name..." />
        <div class="spacer"></div>
        <button onclick="displayTopResult()" class="SEARCH">Search</button>
      </div>
    </div>

    JavaScript

    The functionality of the tool is then created with JavaScript.

    Assigning Variables

    First I’ve declared the main variables which will be used in tool.

      const container = document.getElementById("container");
      let songID;
      let accesstoken;
      let bpmResult;
      let searchResultsArray;
      let searchTerm;
      let artistName;
      let songName;
    
    

    Handling User Input

    To ensure that the search term inputted by the user is assigned to the variable ‘searchTerm’ by the time that it is used, I’ve added an event listener that updates the searchTerm variable after each time character is typed in the search box.

      const searchTermInput = document.getElementById("songSearch");
      if (searchTermInput) {
        searchTermInput.addEventListener("input", function (event) {
          searchTerm = event.target.value;
        });
      } else {
        console.error("the search input not found");
      }

    Token Retrieval

    To keep the client ID and client secret hidden, the token retrieval request is called from the backend using PHP. The access token is then returned to the front end, parsed as JSON and assigned to the variable ‘accesstoken’

    Token Retrieval (front end)

      function getToken() {
        return fetch("/Website_dependencies/Spotify BPM Finder v13 PHP.php")
          .then((response) => response.json())
          .then((data) => {
            accesstoken = data.access_token;
            return accesstoken;
          })
          .catch((error) => {
            console.error("Error fetching access token:", error);
          });
      }

    Token Retrieval (Back End)

    <?php
    
    $clientID = "YOUR_CLIENT_ID";
    $clientSecret = "YOUR_CLIENT_SECRET";
    
    $data = array(
        'grant_type' => 'client_credentials'
    );
    
    $encodedCredentials = base64_encode($clientID . ":" . $clientSecret);
    
    $ch = curl_init();
    
    curl_setopt($ch, CURLOPT_URL, "https://accounts.spotify.com/api/token");
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
        "Content-Type: application/x-www-form-urlencoded",
        "Authorization: Basic $encodedCredentials"
    ));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    curl_close($ch);
    
    echo $response;
    ?>
    

    Retrieving Search Results

    The below function retrieves the first search result based on the what the end user has entered. It works by first running the getToken function and using that access code to send a request to the Spotify search endpoint. The result is then parsed as JSON and the variables for artistName, songName and songID are assigned from the first object in the array of tracks that is returned.

    function getTopResult() {
        return getToken()
          .then(() => {
            return fetch(
              `https://api.spotify.com/v1/search?q=${searchTerm}&type=track&limit=5`,
              {
                method: "GET",
                headers: {
                  Authorization: `Bearer ${accesstoken}`,
                },
              }
            );
          })
          .then((response) => {
            return response.json();
          })
          .then((jsonresponse) => {
            searchResultsArray = jsonresponse.tracks.items;
            artistName = searchResultsArray[0].artists[0].name;
            songName = searchResultsArray[0].name;
            songID = searchResultsArray[0].uri.split(":").pop();
            return searchResultsArray[0];
          });
      }

    Retrieving Tempo Information

    To obtain the BPM, the below function sends a GET request with the song ID that was assigned in the previous function to the audio-features endpoint. The result is parsed as JSON and the ‘tempo’ property is retrieved, rounded and assigned to the variable bpmResult.

      function getBPM() {
        return fetch(`https://api.spotify.com/v1/audio-features/${songID}`, {
          method: "GET",
          headers: {
            Authorization: `Bearer ${accesstoken}`,
          },
        })
          .then((featuresresponse) => featuresresponse.json())
          .then((featuresjsondata) => {
            bpmResult = Math.round(featuresjsondata.tempo);
            return bpmResult;
          });
      }

    Displaying The Results

    The below function is set to run whenever the ‘enter’ key is pressed or the ‘search’ button is clicked on. It begins by clearing all the HTML in the div, calls both the functions getTopResult and getBPM and then creates new HTML displaying the results.

    The metronome is also initialized at this point (see details in the next section).

      function displayTopResult() {
        container.innerHTML = "";
        getTopResult()
          .then(() => {
            return getBPM();
          })
          .then(() => {
         container.innerHTML = `
                <p class="results">
                  <span class="song-name">${songName}</span>
                  <span class="by"> by </span>
                  <span class="artist-name">${artistName}</span><br><br>
                  <span id="bpmm" class="bpm">${bpmResult}</span>
                  <span class="by2">bpm</span>
                  <br>
                  <span id="bpmmm" class="metronome"> metronome paused<br> (press the big number to start it)</span>
                  <br>
                  <br>
                  <br>
                  <button onclick="location.reload()" class="startOver">Start Over</button>
                  </p>
        `;
                  initializeMetronome();
          });
      }

    The function is called when pressing the enter key by adding an event listener at the bottom of the code:

      searchTermInput.addEventListener("keydown", function (event) {
        if (event.key === "Enter") {
          displayTopResult();
        }
      });

    Adding a Metronome

    As an extra touch I created a metronome that plays when you touch the number. This allows you to test out the tempo and play along to it straight away. I created it using a recording of a single drum hit, which then triggers repeatedly at a certain interval based on the outcome of the search.

    It’s achieved using a number of functions that are housed within a parent function ‘initializeMetronome’ which is called at the same point that the search results are displayed.

    function initializeMetronome() {
        let intervalId;
        let isPlaying = false;
        let metronomeBuffer; // Variable to store the loaded audio buffer
        let audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const startStopButton = document.getElementById("bpmm");
    
        function startMetronome() {
          isPlaying = true;
          playMetronomeSound(); // Start the initial buffer playback instance
    
          intervalId = setInterval(playMetronomeSound, 60000 / bpmResult);
          document.getElementById("bpmmm").innerHTML =
            "metronome playing <br> (press the big number to stop it)";
        }
    
        function stopMetronome() {
          isPlaying = false;
          clearInterval(intervalId);
          document.getElementById("bpmmm").innerHTML =
            "metronome paused<br> (press the big number to start it)";
        }
    
        function loadMetronomeSound() {
          return fetch(
            "/Website_dependencies/Spotify BPM Finder v17 PHP.php?action=getMetronome"
          )
            .then((response) => {
              console.log("Response:", response);
              return response.arrayBuffer();
            })
            .then((arrayBuffer) => {
              console.log("Array Buffer:", arrayBuffer);
              return audioContext.decodeAudioData(arrayBuffer);
            })
            .then((buffer) => {
              console.log("Audio Buffer:", buffer);
              metronomeBuffer = buffer;
            })
            .catch((error) => {
              console.error("Error loading metronome sound:", error);
            });
        }
        function playMetronomeSound() {
          let source = audioContext.createBufferSource();
          source.buffer = metronomeBuffer;
          source.connect(audioContext.destination);
          source.start();
        }
    
        loadMetronomeSound();
    
        startStopButton.addEventListener("click", toggleMetronome);
    
        function toggleMetronome() {
          if (!isPlaying) {
            startMetronome();
          } else {
            stopMetronome();
          }
        }
      }

    I opted to load the sound via an API request using the fetch function and a buffer, rather than loading it straight as when playing the metronome on a mobile device issues with latency would cause the tempo to play incorrectly. Additionally the metronome sound file is loaded from the PHP file which sits on the server to avoid sharing the API key.

    Styling

    Finally, I styled the elements with CSS to make the tool visually appealing. Elements such as the search container, buttons, and result display are animated for a smoother user experience. The use of keyframes in CSS provides a pleasant fade-in effect and adds a rotating animation to the BPM display.

    You can find the CSS used here.

    Summary

    In summary, this BPM finder uses the Spotify API and JavaScript to create an interactive and visually appealing user interface. The CSS styling enhances the overall experience, and the JavaScript code efficiently handles user input, API calls, result display and creation of a metronome.

    Feel free to customize or use the code as you please in your own projects!

  • Analyzing Customer Spending Habits

    Introduction

    The purpose of this report is to analyze an open source dataset containing customer sales data, idenfity any notable findings and present insights which can be used to inform marketing efforts.

    The dataset used contains transaction data from customer purchases from multiple countries during the period of Jan 2015 to July 2016 and includes data such as gender, age, product category, quantity. It also includes the cost price and retail price of each item purchase.

    Note: Some details are not included in this dataset such as the merchant, the exact brand/item and a way to identify repeat customers or instances where a customer has bought more than one different item.1 This report works on the assumption the sales data pertains to one company. This conclusions of this report are therefore limited.

    Dataset Source Url

    https://www.kaggle.com/datasets/thedevastator/analyzing-customer-spending-habits-to-improve-sa?resource=download

    Importing and making sense of data

    I imported the data into a local MySQL server and accessed it with MySQL workbench. As the data was already clean there was no need to preprocess it. It could be analyzed straight away.

    Here shows all columns, showing the first 5 results:

    Generated by wpDataTables

    To begin the analysis, I separated the data into different demographics – distinguishing by country, age group and gender. I added columns to show the total number of customers in each demographic, along with the average revenue and profit, and the total revenue and profit.

    
    SELECT Country, Gender, 
    CASE WHEN age between 17 and 24 then '17-24'
       when age between 25 and 34 then '25-34'
       when  age >= 35 then '35+' END AS Age_group, 
    ROUND(SUM(Revenue)/1000,1) AS `Total_Revenue_(k)`, 
    ROUND(SUM(Revenue-Cost)/1000) AS `Total_Profit_(k)`,
    ROUND(AVG(Revenue),1) AS `AvgRevenue`,
    ROUND(AVG(100*(Revenue-Cost)/Revenue),1) 
    AS `Avg_Profit_margin_%`,  
    COUNT(*) AS Total_customers
    FROM Sales_data
    GROUP BY Country, Gender, Age_group
    ORDER BY COUNTRY ASC
    Generated by wpDataTables

    Insights

    Analyzing the data we can find the following insights.

    Germany is the most profitable region

    Germany generates the most profit (959k) with an average profit margin of 26.6% (more than double the second most profitable, UK with 12.4%). This is true even though it generates less than half the revenue of the United States (Germany 4.2K, United States 10.4K).

    It has around the same number of customers as France (5.2k) while generating 3.5x the profit.

    Having the highest average spent per purchase supports the stereotype that German customers prefer to spend more to obtain higher quality. Emphasizing quality should therefore be a prominent theme in the marketing messaging.

    Bikes are making money in Germany but not anywhere else

    Analyzing the profit of different product categories we can see that bikes overall have an average profit of 1% compared to 14.8% or 17% for clothing or accessories.

    Delving deeper it becomes clear there is a clear discrepancy between countries – Germany is maintaining a healthy profit margin(16.4%) while the UK, France and the United States are not (0% to -3.5%).

    The United States is the only region to have lost money (-109K), with France and the UK making a measly profit ( 15K and 16K respectively – compared to 416K for Germany).

    This suggests that products are being sold at heavily discounted prices in all regions except Germany.

    Aside from the lack of profitability, as the other countries have comparable sales volume to Germany it suggests the market for purchasing bikes still exists in the less profitable regions. It would be recommended to experiment with lower priced brands/models as the data suggests France, the UK and the US are prepared to pay less than Germany to buy a bike.

    Sales rise in the summer, and slump in the autumn

    Visualizing in Tableau the total revenue by month for each product category shows a notable spike in bike sales during May and June. It’s followed by a lull and slow recovery, increasing momentum towards Christmas. Sales of accessories and clothing follow suit but to a lesser extent.

    In the below chart, the Y-axis shows revenue while the thickness of the line shows the relative quantity.

    Looking deeper into subcategories – many stay more or less the same while some (e.g fenders and gloves) are affected by the seasonal trend, as shown in this heat map:

    It would be recommended therefore to reduce marketing budget for bikes, fenders and gloves during the low months August to October and instead allocate this budget to the other months, in order to maximise the momentum of the natural trend.

    Footnotes

    1. We can see this as for each data entry where there is quantity greater than one, the total revenue of the purchase is an exact multiple of the price, confirming the entry is referring to the same product. We can confirm this by the following query, which should produce a zero results:

      SELECT * FROM Sales_data
      WHERE Revenue NOT IN (
      SELECT unit_price * quantity
      FROM Sales_data)


      This presents numerous results. I noticed it was caused by each one being not quite an exact match – the unit price being a third or two thirds of the revenue but rounded to 3 decimal places. Amended, the query below accounts for this:

      SELECT * FROM Sales_data
      WHERE Revenue NOT BETWEEN (unit_price * quantity) - 0.1 AND (unit_price * quantity) + 0.1


      The result is 0 rows returned. Interesting, as the unit price is showing a very specific decimal it makes likely these items are sold as sets of three.