Your API Key

Use this API key to authenticate your requests to our B2B API. Keep it secure and do not share it publicly.

API Documentation

Database Schema

Introduction

This guide explains how to set up the backend for your audio mastering API. It covers how to handle user data, song uploads, and mastering jobs. The schemas provided below are designed for MongoDB with Mongoose and include relationships between users, songs, and mastering jobs.

User Schema

The User schema stores user-specific information, such as API keys, email, and subscription status. This data is critical for authenticating API requests and tracking user access.


const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  apiKey: { type: String, required: true, unique: true },
  subscriptionStatus: { type: String, enum: ['active', 'inactive'], default: 'inactive' },
  createdAt: { type: Date, default: Date.now },
});

module.exports = mongoose.model('User', userSchema);
  

Song Schema

The Song schema stores information related to each song that users upload for mastering. It tracks the song's metadata, upload status, and the URLs of the mastered and original files stored in S3.


const mongoose = require('mongoose');

const songSchema = new mongoose.Schema({
  userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  title: { type: String, required: true },
  extension: { type: String, required: true },
  size: { type: Number, required: true },
  duration: { type: Number, required: true },
  status: { type: String, enum: ['pending', 'processing', 'mastered'], default: 'pending' },
  uploadDate: { type: Date, default: Date.now },
  masteredUrl: { type: String },
  originalUrl: { type: String },
  masteredPeaks: [Number],
  originalPeaks: [Number],
});

module.exports = mongoose.model('Song', songSchema);
  

Mastering Job Schema

The MasteringJob schema tracks the status of each mastering job, including processing time, the result URL, and timestamps for job creation and updates.


const mongoose = require('mongoose');

const masteringJobSchema = new mongoose.Schema({
  songId: { type: mongoose.Schema.Types.ObjectId, ref: 'Song', required: true },
  status: { type: String, enum: ['queued', 'processing', 'completed', 'failed'], default: 'queued' },
  processingTime: { type: Number }, // In seconds
  resultUrl: { type: String }, // URL to the mastered file
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now },
});

module.exports = mongoose.model('MasteringJob', masteringJobSchema);
  

AWS S3 File Upload (Backend API: `/api/get-signed-url`)

Introduction

Welcome to the API documentation for our mastering service. Follow this guide to learn how to upload audio files to your own AWS S3 bucket and send the file details to our mastering API.

Step 1: AWS S3 File Upload (Backend API: `/api/get-signed-url`)

Use your own AWS credentials to generate a signed URL and upload audio files to your S3 bucket. The following code snippet shows how to configure AWS SDK and generate a signed URL. You will need to implement a backend API (e.g., `/api/get-signed-url`) to securely generate the signed URL using the AWS SDK and return it to your frontend.

import AWS from 'aws-sdk';
import { v4 as uuid } from 'uuid';

const generateSignedUrl = (file, userId) => {
  const uid = uuid();
  const fileExtension = file.name.split('.').pop();

  const s3 = new AWS.S3({
    region: 'your-region', // e.g., 'us-east-1'
    accessKeyId: 'YOUR_AWS_ACCESS_KEY', // Client's AWS Access Key
    secretAccessKey: 'YOUR_AWS_SECRET_KEY', // Client's AWS Secret Key
  });

  const signedUrl = s3.getSignedUrl('putObject', {
    Bucket: 'your-s3-bucket', // Client's S3 bucket
    Key: `${userId}/${uid}/${file.name}`, // S3 path
    Expires: 600, // Signed URL expiration time
    ContentType: file.type,
  });

  return { signedUrl, uid, fileExtension };
};

Once you generate the signed URL from the backend API, you can upload the file to your S3 bucket using the following Axios request from the frontend. (Example)

import axios from 'axios';

const uploadFileToS3 = async (file, signedUrl) => {
  try {
    const upload = await axios.put(signedUrl, file, {
      headers: {
        'Content-Type': file.type,
      },
    });
    console.log("File uploaded successfully:", upload);
  } catch (error) {
    console.error("Error uploading file:", error);
  }
};

Step 2: Submit for Mastering (Backend API: `/api/submit-for-mastering`)

After the file has been successfully uploaded to your S3 bucket, send the file details to our mastering API to begin processing. You will need to implement a backend API (e.g., `/api/submit-for-mastering`) to handle this submission and interface with the mastering service.

import axios from 'axios';

const submitForMastering = async (s3Key, bucket, fileExtension, apiKey) => {
  const requestData = {
    s3Key: s3Key,
    bucket: bucket,
    title: "My Track Title",
    ext: fileExtension,
    size: "5.5",  // Optional: size in MB
    duration: "180",  // Optional: duration in seconds
  };

  try {
    const response = await axios.post("https://chosenmasters.com/api/masteringApi", requestData, {
      headers: {
        Authorization: `Bearer ${apiKey}`,
      },
    });
    console.log("Mastering job created:", response.data);
  } catch (error) {
    console.error("Error during mastering:", error);
  }
};

Try It Out: Dropzone Upload

Use the following file upload dropzone to test uploading a file to your AWS S3 bucket and submitting it for mastering.

Full React Component Example

Below is a complete React component for uploading a file to AWS S3 and submitting the file details to your mastering API. You can copy and paste this code into your project. Make sure you have the correct AWS credentials set in your .env file and that you have implemented the APIs listed below:

  • API 1: Generate Signed URL (Backend Endpoint: /api/get-signed-url): This API is used to generate the signed URL for securely uploading files to your AWS S3 bucket. You will need to implement this in your backend, using AWS SDK to generate the signed URL.
  • API 2: Submit for Mastering (Backend Endpoint: /api/submit-for-mastering): After the file has been uploaded to S3, this API takes the file details (such as the S3 key, bucket, file size, etc.) and submits them for mastering. This will trigger the mastering process using your mastering service API.

Below is the full React component that ties everything together:

import React, { useState, useCallback } from "react";
import { useDropzone } from "react-dropzone";
import axios from "axios";
import AWS from "aws-sdk";
import { v4 as uuid } from "uuid";

const MasteringUploadComponent = ({ apiKey, awsAccessKeyId, awsSecretAccessKey, bucketRegion, bucketName }) => {
  const [dropError, setDropError] = useState("");
  const [uploadProgress, setUploadProgress] = useState(0);
  const [uploadComplete, setUploadComplete] = useState(false);

  const handleUpload = useCallback(
    async (acceptedFiles) => {
      if (acceptedFiles.length === 0) {
        setDropError("No file selected.");
        return;
      }

      const file = acceptedFiles[0];
      const fileName = file.name;
      const fileType = file.type;

      try {
        // Step 1: Request signed URL from your backend
        const signedUrlResponse = await axios.post("/api/get-signed-url", {
          fileName,
          fileType,
        });

        const { signedUrl, s3Key, bucket } = signedUrlResponse.data;

        // Step 2: Upload the file to S3 using the signed URL
        const upload = await axios.put(signedUrl, file, {
          headers: {
            "Content-Type": file.type,
          },
          onUploadProgress: (progressEvent) => {
            const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
            setUploadProgress(progress);
          },
        });

        if (upload.status === 200) {
          // Step 3: Submit the file for mastering
          const response = await axios.post(
            "/api/submit-for-mastering",
            {
              s3Key, // This is the S3 key returned from the signed URL
              bucket, // S3 bucket name
              title: file.name.split(".")[0], // Track title
              ext: file.name.split(".").pop(), // File extension
              size: (file.size / 1048576).toFixed(2), // Size in MB
              apiKey, // User's API key
            }
          );

          if (response.status === 200) {
            console.log("Mastering job created successfully.");
            setUploadComplete(true);
          }
        }
      } catch (error) {
        console.error("Error during upload or mastering:", error);
        setDropError("Upload failed. Please try again.");
      }
    },
    [apiKey]
  );

  const { getRootProps, getInputProps } = useDropzone({
    onDrop: handleUpload,
    accept: "audio/*",
    maxFiles: 1,
  });

  return (
    <div>
      <div {...getRootProps({ className: "dropzone bg-gray-200 p-8 rounded-lg mb-4 dark:bg-gray-800" })}>
        <input {...getInputProps()} />
        <p>Drag and drop an audio file here, or click to select a file</p>
      </div>

      {dropError && <p className="text-red-500">{dropError}</p>}

      {uploadProgress > 0 && (
        <div className="w-full bg-gray-200 rounded-full h-4 mb-4">
          <div className={`h-4 rounded-full ${uploadComplete ? "bg-green-500" : "bg-blue-500"}`} style={{ width: `${uploadProgress}%` }}></div>
        </div>
      )}

      {uploadComplete && <p className="text-green-600 text-center mt-4">Upload Complete!</p>}
    </div>
  );
};

export default MasteringUploadComponent;

Song List API: /api/listSongs

Introduction

This API fetches a user's uploaded songs from AWS S3, organizes them by folders, and implements pagination to handle large data sets. It connects to MongoDB to retrieve song metadata and sorts the songs by their last modified date.

API Code

import AWS from "aws-sdk";
import masteringModel from "../../models/mastering.model";
import dbConnect from "../../db/db"; // Adjust the path to your dbConnect file

export default async function handler(req, res) {
  if (req.method === "GET") {
    try {
      await dbConnect();

      const { userId, page = 1, limit = 5 } = req.query;
      const pageNum = parseInt(page, 10);
      const limitNum = parseInt(limit, 10);

      AWS.config.update({
        region: process.env.YOUR_AWS_BUCKET_REGION,
        accessKeyId: process.env.YOUR_AWS_ACCESS_KEY,
        secretAccessKey: process.env.YOUR_AWS_SECRET_ACCESS_KEY,

      });

      const s3 = new AWS.S3();
      const allObjects = [];
      let continuationToken = null;

      // Use pagination to handle large datasets
      do {
        const params = {
          Bucket: process.env.YOUR_S3_BUCKET_NAME,
          Prefix: `${userId}/`,
          ContinuationToken: continuationToken,
        };

        const data = await s3.listObjectsV2(params).promise();
        allObjects.push(...data.Contents);
        continuationToken = data.IsTruncated ? data.NextContinuationToken : null;
      } while (continuationToken);

      const folders = {};
      const songIds = new Set();

      // Collect all songIds and organize them into folders
      for (const object of allObjects) {
        const keyParts = object.Key.split("/");
        const allowedExtensions = [".wav", ".mp3", ".aiff", ".flac"];

        // Ensure the object is a song file
        if (
          keyParts.length === 3 &&
          allowedExtensions.some((ext) => keyParts[2].endsWith(ext))
        ) {
          const folderName = keyParts[1];
          const songId = keyParts[2].split(".")[0];
          const lastModified = object.LastModified;

          if (!folders[folderName]) {
            folders[folderName] = {
              subFolders: [],
              isMastered: false,
            };
          }

          folders[folderName].subFolders.push({ songId, lastModified });
          songIds.add(songId);
        }
      }

      const songIdsArray = Array.from(songIds);

      // Use projection to fetch only necessary fields
      const allSongs = await masteringModel
        .find({
          uid: { $in: songIdsArray },
          user: userId,
        })
        .select("uid title mastered") // Select only necessary fields
        .lean();

      const masteredSongIds = new Set(
        allSongs.filter((song) => song.mastered).map((song) => song.uid)
      );

      // Create a map of songId to title for all songs
      const songIdToTitle = allSongs.reduce((acc, song) => {
        acc[song.uid] = { title: song.title, mastered: song.mastered };
        return acc;
      }, {});

      for (const folderName in folders) {
        // Assign title and mastered status from the song data using songId
        folders[folderName].subFolders.forEach((subFolder) => {
          const songData = songIdToTitle[subFolder.songId];
          if (songData) {
            subFolder.title = songData.title;
            if (songData.mastered) {
              folders[folderName].isMastered = true; // Set isMastered if any song in the folder is mastered
            }
          }
        });

        // Filter out subFolders with title 'sample'
        folders[folderName].subFolders = folders[folderName].subFolders.filter(
          (subFolder) => subFolder.title !== "sample"
        );

        // Sort subFolders by lastModified date
        folders[folderName].subFolders.sort(
          (a, b) => new Date(b.lastModified) - new Date(a.lastModified)
        );
      }

      // Filter out any folders with an empty subFolders array
      const filteredFolders = Object.keys(folders)
        .map((folderName) => ({
          name: folderName,
          subFolders: folders[folderName].subFolders,
          isMastered: folders[folderName].isMastered,
        }))
        .filter((folder) => folder.subFolders.length > 0)
        .sort(
          (a, b) =>
            new Date(b.subFolders[0]?.lastModified) -
            new Date(a.subFolders[0]?.lastModified)
        );

      // Implement pagination
      const startIndex = (pageNum - 1) * limitNum;
      const paginatedFolders = filteredFolders.slice(
        startIndex,
        startIndex + limitNum
      );

      res.status(200).json({
        success: true,
        folders: paginatedFolders,
        page: pageNum,
        limit: limitNum,
        totalPages: Math.ceil(filteredFolders.length / limitNum),
        totalItems: filteredFolders.length,
        hasMore: startIndex + limitNum < filteredFolders.length,
      });
    } catch (error) {
      console.error("An internal error occurred:", error);
      res
        .status(500)
        .json({ success: false, message: "An internal error occurred" });
    }
  } else {
    res.setHeader("Allow", ["GET"]);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

How It Works

  • The API connects to AWS S3 using the AWS SDK to retrieve files based on the user's ID.
  • The data is organized into folders and subfolders, where each subfolder represents a song file.
  • The MongoDB database is used to fetch metadata for each song, such as title and whether the song is mastered or not.
  • The API implements pagination to handle large datasets efficiently.

Usage

You can integrate this API into your application to fetch and display users' uploaded songs with metadata and pagination support.

Song List Loading Component

The SongListLoading component helps fetch the mastered files from S3 and passes the data to other components like SongCollectionLoading for the correct track to be loaded and played. It handles retry logic, pagination, and rendering of song options.

The SongListLoading component provides the list of available songs and handles the loading of song metadata from S3 or other storage services. Once the list is populated, the component works together with SongCollectionLoading to pass the selected song data and allow the user to play or download the tracks. Ensure you connect these components properly by passing the selected song from SongListLoading to SongCollectionLoading using the appropriate props such as onSelectSong and userId.

import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
import { useSession } from "next-auth/react";
import toast from "react-hot-toast";
import Rive from "rive-react";
import SongItem from "./SongItem";

const SongListLoading = ({ onSelectSong, enableAutoSelect }) => {
  const [folders, setFolders] = useState([]);
  const [loading, setLoading] = useState(true);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [totalPages, setTotalPages] = useState(0);
  const [fetchFailed, setFetchFailed] = useState(false);
  const isDataFetched = useRef(false);
  const { data: session } = useSession();
  const userId = session?.user?.id;
  const email = session?.user?.email;

  const fetchFolders = async (retry = 0, isRetryButton = false) => {
    setLoading(true);
    try {
      const response = await axios.get('/api/listSongs', {
        params: { page, limit: 5, userId, email }
      });
      if (response.status === 200) {
        setFolders(response.data.folders);
        setHasMore(response.data.hasMore);
        setTotalPages(response.data.totalPages);
        setLoading(false);
        toast.success('Song loaded successfully!');
        setFetchFailed(false);
        isDataFetched.current = true;
      } else {
        throw new Error('Failed to fetch folders');
      }
    } catch (error) {
      if (retry < 3 && !isRetryButton) {
        setTimeout(() => fetchFolders(retry + 1), 1000 * Math.pow(2, retry));
      } else {
        console.error(error);
        toast.error('Unable to load data after multiple attempts.');
        setLoading(false);
        setFetchFailed(true);
        setTimeout(() => window.location.reload(), 100);
      }
    }
  };

  useEffect(() => {
    if (session?.user?.id && !isDataFetched.current) {
      fetchFolders();
    }
  }, [session, page]);

  return (
    <div>
      <div className="loading-container">
        {loading && <Rive src="https://public.rive.app/community/runtime-files/65-358-iron-giant-demo.riv" />}
        {fetchFailed && <button onClick={handleRetryFetch}>Retry</button>}
        {folders.map((folder) => (
          <SongItem key={folder.id} song={folder} />
        ))}
        <div>
          <button disabled={page <= 1} onClick={handlePrevPage}>Previous</button>
          <button disabled={!hasMore} onClick={handleNextPage}>Next</button>
        </div>
      </div>
    </div>
  );
};

export default SongListLoading;

Connecting SongListLoading with SongCollectionLoading

To connect SongListLoading with SongCollectionLoading, pass the selected song data from the list component to the collection component. The onSelectSong prop in SongListLoading should call a function that updates the SongCollectionLoading component, allowing it to load and play the selected track. Below is an example of how you can pass data between these components:

// Example of connecting SongListLoading to SongCollectionLoading
const MySongPage = () => {
  const [selectedSong, setSelectedSong] = useState(null);

  const handleSelectSong = (songId, isMastered) => {
    setSelectedSong({ songId, isMastered });
  };

  return (
    <div>
      <SongListLoading onSelectSong={handleSelectSong} />
      {selectedSong && (
        <SongCollectionLoading 
          selectedSong={selectedSong.songId} 
          isMastered={selectedSong.isMastered} 
          userId="USER_ID_HERE" 
        />
      )}
    </div>
  );
};

Song Collection Loading Documentation

Introduction

This component demonstrates how to load mastered files, allow users to play and download them, toggle between original and mastered audio, and control playback settings like intensity and zoom. Below is the full code for the component and its documentation.

Component Overview

The `SongCollectionLoading` component integrates the following features: - Loads songs from a user's collection. - Uses `WaveSurfer.js` to visualize audio waveforms. - Allows users to toggle between original and mastered versions of the song. - Provides controls for audio playback, intensity/volume, and zoom. - Allows downloading the current song in a specific format.


import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import WaveSurfer from 'wavesurfer.js';
import { useSession } from 'next-auth/react';

const SongCollectionLoading = ({ userId, onSongSelected, enableAutoSelect }) => {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(false);
  const [selectedSong, setSelectedSong] = useState(null);
  const [waveSurferReady, setWaveSurferReady] = useState(false);
  const waveSurfer = useRef(null);
  const waveformRef = useRef(null);

  const [mode, setMode] = useState('1'); // Mode selection (e.g., "Open", "Warm")
  const [volume, setVolume] = useState(3); // Intensity/Volume
  const [isPlaying, setIsPlaying] = useState(false);
  const [isOriginal, setIsOriginal] = useState(false); // Toggle between original and mastered
  const [isMastered, setIsMastered] = useState(false); // Track if the song is mastered
  const [showDownloadOptions, setShowDownloadOptions] = useState(false); // Toggle for download options

  const [currentTime, setCurrentTime] = useState(0);
  const [totalDuration, setTotalDuration] = useState(0);
  const [zoomLevel, setZoomLevel] = useState(100); // For zooming the waveform
  const [audioUrl, setAudioUrl] = useState('');
  const [masteredPlaybackPosition, setMasteredPlaybackPosition] = useState(0); // Keep track of where we paused on mastered files

  const { data: session } = useSession();

  const handleDownload = async (format) => {
    const modeString = mode === '0' ? 'Open' : 'Warm';
    const audioExtension = format;
    const downloadPath = `${userId}/${selectedSong}/${selectedSong}.wav/mode_${mode}/demo/${selectedSong}_V${volume}_${modeString}_chosenmasters.${audioExtension}`;

    try {
      const response = await axios.get(`/api/getSignedUrl?fileKey=${encodeURIComponent(downloadPath)}`);
      if (response.status === 200 && response.data.url) {
        const link = document.createElement('a');
        link.href = response.data.url;
        link.setAttribute('download', `${selectedSong}_V${volume}_${modeString}_chosenmasters.${audioExtension}`);
        document.body.appendChild(link);
        link.click();
        link.remove();
      } else {
        console.error('File not available for download or issue fetching signed URL');
      }
    } catch (error) {
      console.error('Error initiating download:', error);
    }
  };

  const initializeWaveSurfer = () => {
    if (selectedSong) {
      if (waveSurfer.current) {
        waveSurfer.current.destroy();
      }
      waveSurfer.current = WaveSurfer.create({
        container: waveformRef.current,
        waveColor: 'white',
        progressColor: 'black',
        height: 300,
        normalize: true,
      });
      waveSurfer.current.on('ready', () => {
        setTotalDuration(waveSurfer.current.getDuration());
        if (isPlaying) {
          waveSurfer.current.play();
        }
      });
      loadSelectedSong(selectedSong);
    }
  };

  const loadSelectedSong = async (songId) => {
    console.log("Attempting to play song with ID:", songId, "Mode:", mode, "Volume:", volume);

    const isSafari = () => {
      return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
    };

    const modeString = mode === '0' ? 'Open' : 'Warm';
    const audioExtension = isSafari() ? 'mp3' : 'ogg'; // Adjust based on Safari detection

    const audioFilePath = isOriginal
      ? `${userId}/${songId}/${songId}.wav`
      : `${userId}/${songId}/${songId}.wav/mode_${mode}/demo/${songId}_V${volume}_${modeString}_chosenmasters.${audioExtension}`;
    const jsonFilePath = isOriginal
      ? `${userId}/${songId}/${songId}.wav/original_upload.json`
      : `${userId}/${songId}/${songId}.wav/mode_${mode}/demo/json/${songId}_V${volume}_${modeString}_chosenmasters.json`;

    const currentTime = waveSurfer.current ? waveSurfer.current.getCurrentTime() : 0;

    try {
      const audioUrlResponse = await axios.get(`/api/getSignedUrl?fileKey=${encodeURIComponent(audioFilePath)}`);
      const audioUrl = audioUrlResponse.data.url;
      setAudioUrl(audioUrl);

      const waveformJsonUrlResponse = await axios.get(`/api/getSignedUrl?fileKey=${encodeURIComponent(jsonFilePath)}`);
      const waveformJsonUrl = waveformJsonUrlResponse.data.url;

      const waveformResponse = await axios.get(waveformJsonUrl);
      const peaks = waveformResponse.data.data;

      waveSurfer.current.load(audioUrl, peaks);

      waveSurfer.current.on('ready', () => {
        waveSurfer.current.seekTo(currentTime / waveSurfer.current.getDuration());
        if (!isOriginal && !isMastered) {
          waveSurfer.current.seekTo(currentTime / waveSurfer.current.getDuration() || 0);
        }
        if (isMastered || isOriginal) {
          waveSurfer.current.seekTo(currentTime / waveSurfer.current.getDuration());
        }
        if (isPlaying) {
          waveSurfer.current.play();
        }
      });
    } catch (error) {
      console.error("Error loading song:", error);
    }
  };

  const handlePlayClick = () => {
    if (waveSurfer.current) {
      waveSurfer.current.playPause();
      setIsPlaying(!isPlaying);
    }
  };

  const toggleIsOriginal = () => {
    if (!isOriginal && waveSurfer.current) {
      const currentPosition = waveSurfer.current.getCurrentTime() / waveSurfer.current.getDuration();
      setMasteredPlaybackPosition(currentPosition);
    }
    setIsOriginal(!isOriginal);
  };

  useEffect(() => {
    initializeWaveSurfer();
  }, [selectedSong]);

  useEffect(() => {
    if (!isOriginal) {
      initializeWaveSurfer();
    }
  }, [isOriginal]);

  useEffect(() => {
    if (waveSurfer.current && waveSurferReady) {
      waveSurfer.current.zoom(zoomLevel);
    }
  }, [zoomLevel, waveSurferReady]);

  useEffect(() => {
    if (waveSurfer.current) {
      waveSurfer.current.on('audioprocess', (time) => {
        setCurrentTime(time);
      });

      waveSurfer.current.on('ready', () => {
        setTotalDuration(waveSurfer.current.getDuration());
      });
    }

    return () => {
      if (waveSurfer.current) {
        waveSurfer.current.un('audioprocess');
        waveSurfer.current.un('ready');
      }
    };
  }, [waveSurfer.current]);

  const handleSelectSong = (songId, isMastered) => {
    setSelectedSong(songId);
    setIsMastered(isMastered);
    if (onSongSelected) {
      onSongSelected(songId, isMastered);
    }
  };

  useEffect(() => {
    if (waveSurfer.current) {
      waveSurfer.current.on('play', () => setIsPlaying(true));
      waveSurfer.current.on('pause', () => setIsPlaying(false));
      waveSurfer.current.on('finish', () => setIsPlaying(false));
    }

    return () => {
      if (waveSurfer.current) {
        waveSurfer.current.un('play');
        waveSurfer.current.un('pause');
        waveSurfer.current.un('finish');
      }
    };
  }, [waveSurfer.current]);

  const formatTime = (time) => {
    const minutes = Math.floor(time / 60);
    const seconds = Math.floor(time % 60);
    return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
  };

  return (
    <div className="container mx-auto p-6 bg-gray-900 text-white rounded-lg shadow-lg">
      <h1 className="text-2xl font-semibold text-center mb-4">Mastering Player with Waveform</h1>

      {/* Waveform */}
      <div ref={waveformRef} className="mb-6"></div>

      {/* Mastered/Pre-Mastered Toggle */}
      <div className="mb-6">
        <label className="block text-lg font-medium mb-2">
          {isMastered ? 'Mastered' : 'Pre-Mastered'}
        </label>
        <button
          onClick={toggleIsOriginal}
          className="px-6 py-2 bg-gray-600 hover:bg-blue-700 rounded-full"
        >
          Toggle to {isMastered ? 'Pre-Mastered' : 'Mastered'}
        </button>
      </div>

      {/* Volume Slider */}
      <div className="mb-6">
        <label htmlFor="volumeSlider" className="block text-lg font-medium mb-2">
          Intensity
        </label>
        <input
          id="volumeSlider"
          type="range"
          min="1"
          max="10"
          value={volume}
          className="w-full"
          onChange={(e) => setVolume(e.target.value)}
        />
        <span className="block mt-2 text-center">{`Current Volume: ${volume}`}</span>
      </div>

      {/* Zoom Slider */}
      <div className="mb-6">
        <label htmlFor="zoomSlider" className="block text-lg font-medium mb-2">
          Zoom
        </label>
        <input
          id="zoomSlider"
          type="range"
          min="0"
          max="500"
          value={zoomLevel}
          className="w-full"
          onChange={(e) => setZoomLevel(Number(e.target.value))}
        />
        <span className="block mt-2 text-center">{`Current Zoom: ${zoomLevel}`}</span>
      </div>

      {/* Play/Pause Button */}
      <div className="text-center mb-6">
        <button
          className={`px-6 py-2 rounded-full ${isPlaying ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700'} transition-all duration-200`}
          onClick={handlePlayClick}
        >
          {isPlaying ? 'Pause' : 'Play'}
        </button>
      </div>

      {/* Download Button with Pop-down options */}
      <div className="text-center">
        <button
          className="px-6 py-2 bg-gray-600 hover:bg-gray-700 rounded-full"
          onClick={() => setShowDownloadOptions(!showDownloadOptions)}
        >
          Download
        </button>

        {showDownloadOptions && (
          <div className="bg-gray-800 text-white mt-2 rounded-lg shadow-lg p-4 absolute">
            <button
              className="block w-full text-left px-4 py-2 hover:bg-gray-700 rounded"
              onClick={() => handleDownload('wav')}
            >
              WAV
            </button>
            <button
              className="block w-full text-left px-4 py-2 hover:bg-gray-700 rounded"
              onClick={() => handleDownload('mp3')}
            >
              MP3
            </button>
            <button
              className="block w-full text-left px-4 py-2 hover:bg-gray-700 rounded"
              onClick={() => handleDownload('ogg')}
            >
              OGG
            </button>
          </div>
        )}
      </div>

      <p className="text-center mt-4">{`Current Time: ${formatTime(currentTime)} / ${formatTime(totalDuration)}`}</p>
    </div>
  );
};

export default SongCollectionLoading;

How It Works

Here's a detailed breakdown of the key features:

  • Song Loading: The component fetches song files from the user's collection using an API and loads them into the `WaveSurfer.js` instance.
  • Playback Controls: Users can toggle playback with the "Play" and "Pause" buttons. The component also tracks the playback time and shows the waveform.
  • Intensity/Volume Control: Users can adjust the volume using a slider, which controls the intensity of the audio.
  • Mode Selection: Users can select between different audio modes such as "Warm" and "Open."
  • Zoom: The waveform can be zoomed in or out using a zoom slider.
  • Mastered/Original Toggle: Users can switch between the original and mastered versions of the song.
  • Download: Users can download the current version of the song with the selected mode and intensity settings.

Usage

You can copy the full example code and integrate it into your project to load, play, and download mastered audio files.

Mastering Player Demo UI with Waveform

3
Current Zoom: 100

Song List Documentation

Introduction

The `SongList` component is responsible for fetching and displaying a list of uploaded songs from the server. It includes retry logic, pagination, and loading indicators. Users can select a song from the list, which will then be passed to another component for playback and other actions.

Component Overview

The `SongList` component integrates the following features: - Fetches songs from the backend API `/api/listSongs`. - Handles retries for failed requests with exponential backoff. - Displays a loading spinner while fetching data. - Displays an error message and retry button if fetching fails. - Passes the selected song to the `onSelectSong` callback for further processing in the `SongCollectionLoading` component.

import React, { useEffect, useState, useRef } from 'react';
import axios from 'axios';
import { useSession } from 'next-auth/react';
import toast from 'react-hot-toast';
import { TailSpin } from 'react-loader-spinner';

const MAX_RETRY_COUNT = 3;

const SongList = ({ onSelectSong, enableAutoSelect }) => {
  const [folders, setFolders] = useState([]);
  const [loading, setLoading] = useState(true);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [totalPages, setTotalPages] = useState(0);
  const [fetchFailed, setFetchFailed] = useState(false);
  const isDataFetched = useRef(false);

  const { data: session } = useSession();
  const userId = session?.user?.id;
  const email = session?.user?.email;

  const fetchFolders = async (retry = 0, isRetryButton = false) => {
    setLoading(true);
    try {
      const response = await axios.get(`/api/listSongs`, {
        params: {
          page,
          limit: 5,
          userId,
          email,
        },
      });
      if (response.status === 200) {
        setFolders(response.data.folders);
        setHasMore(response.data.hasMore);
        setTotalPages(response.data.totalPages);
        setLoading(false);
        toast.success("Songs loaded successfully!");
        setFetchFailed(false);
        isDataFetched.current = true;
      } else {
        throw new Error("Failed to fetch folders");
      }
    } catch (error) {
      if (retry < MAX_RETRY_COUNT && !isRetryButton) {
        setTimeout(() => fetchFolders(retry + 1), 1000 * Math.pow(2, retry)); 
      } else {
        toast.error("Unable to load data after multiple attempts.");
        setLoading(false);
        setFetchFailed(true);
        setTimeout(() => window.location.reload(), 100);
      }
    }
  };

  useEffect(() => {
    if (session?.user?.id && !isDataFetched.current) {
      fetchFolders();
    }
  }, [session, page]);

  const handleSelectSong = (songId, isMastered, songTitle) => {
    onSelectSong(songId, isMastered, songTitle);
  };

  return (
    <div className="bg-gray-50 dark:bg-slate-950 hover:bg-gray-100 transition ease-in-out my-6 deep-inner-shadow py-4 px-2 md:px-12 rounded-xl">
      {loading ? (
        <div className="flex justify-center items-center h-40">
          <TailSpin color="#00BFFF" height={50} width={50} />
        </div>
      ) : fetchFailed ? (
        <div className="flex justify-center items-center h-40 flex-col">
          <p className="text-red-500 mb-4">Failed to load songs. Please refresh.</p>
          <button className="bg-blue-500 text-white px-4 py-2 rounded" onClick={() => window.location.reload()}>
            Refresh
          </button>
        </div>
      ) : (
        <ul>
          {folders.map((folder, index) => (
            <li key={folder.id || index}>
              <button onClick={() => handleSelectSong(folder.id, folder.isMastered, folder.title)}>
                {folder.title}
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

export default SongList;

How It Works

Here's a detailed breakdown of the key features:

  • Fetching Data: The `fetchFolders` function retrieves a paginated list of songs for the authenticated user. It retries fetching up to three times if a request fails.
  • Pagination: The component handles pagination, allowing the user to navigate through multiple pages of songs.
  • Song Selection: When a user selects a song from the list, it triggers the `onSelectSong` callback, passing the selected song's ID, mastering status, and title.
  • Auto-select Feature: If `enableAutoSelect` is true, the component automatically selects the first song once the list is loaded.

Usage

You can copy the full example code and integrate it into your project to display a list of songs. Connect it to other components, such as `SongCollectionLoading`, for playback and further interactions.

Uploaded Songs List Example