diff --git a/AudioFile.h b/AudioFile.h index fcd3fd5..3c53243 100644 --- a/AudioFile.h +++ b/AudioFile.h @@ -47,6 +47,9 @@ #include #include #include +#include +#include +#include // disable some warnings on Windows #if defined (_MSC_VER) @@ -90,22 +93,22 @@ class AudioFile AudioFile(); /** Constructor, using a given file path to load a file */ - AudioFile (std::string filePath); + AudioFile (const std::string& filePath); //============================================================= /** Loads an audio file from a given file path. * @Returns true if the file was successfully loaded */ - bool load (std::string filePath); + bool load (const std::string& filePath); /** Saves an audio file to a given file path. * @Returns true if the file was successfully saved */ - bool save (std::string filePath, AudioFileFormat format = AudioFileFormat::Wave); + bool save (const std::string& filePath, AudioFileFormat format = AudioFileFormat::Wave); //============================================================= /** Loads an audio file from data in memory */ - bool loadFromMemory (std::vector& fileData); + bool loadFromMemory (const std::vector& fileData); //============================================================= /** @Returns the sample rate */ @@ -137,26 +140,26 @@ class AudioFile /** Set the audio buffer for this AudioFile by copying samples from another buffer. * @Returns true if the buffer was copied successfully. */ - bool setAudioBuffer (AudioBuffer& newBuffer); + bool setAudioBuffer (const AudioBuffer& newBuffer); /** Sets the audio buffer to a given number of channels and number of samples per channel. This will try to preserve * the existing audio, adding zeros to any new channels or new samples in a given channel. */ - void setAudioBufferSize (int numChannels, int numSamples); + void setAudioBufferSize (const int numChannels, const int numSamples); /** Sets the number of samples per channel in the audio buffer. This will try to preserve * the existing audio, adding zeros to new samples in a given channel if the number of samples is increased. */ - void setNumSamplesPerChannel (int numSamples); + void setNumSamplesPerChannel (const int numSamples); /** Sets the number of channels. New channels will have the correct number of samples and be initialised to zero */ - void setNumChannels (int numChannels); + void setNumChannels (const int numChannels); /** Sets the bit depth for the audio file. If you use the save() function, this bit depth rate will be used */ - void setBitDepth (int numBitsPerSample); + void setBitDepth (const int numBitsPerSample); /** Sets the sample rate for the audio file. If you use the save() function, this sample rate will be used */ - void setSampleRate (uint32_t newSampleRate); + void setSampleRate (const uint32_t newSampleRate); //============================================================= /** Sets whether the library should log error messages to the console. By default this is true */ @@ -185,38 +188,38 @@ class AudioFile }; //============================================================= - AudioFileFormat determineAudioFileFormat (std::vector& fileData); - bool decodeWaveFile (std::vector& fileData); - bool decodeAiffFile (std::vector& fileData); + bool decodeWaveFile (const std::vector& fileData); + bool decodeAiffFile (const std::vector& fileData); //============================================================= - bool saveToWaveFile (std::string filePath); - bool saveToAiffFile (std::string filePath); + bool saveToWaveFile (const std::string& filePath); + bool saveToAiffFile (const std::string& filePath); //============================================================= void clearAudioBuffer(); //============================================================= - int32_t fourBytesToInt (std::vector& source, int startIndex, Endianness endianness = Endianness::LittleEndian); - int16_t twoBytesToInt (std::vector& source, int startIndex, Endianness endianness = Endianness::LittleEndian); - int getIndexOfString (std::vector& source, std::string s); - int getIndexOfChunk (std::vector& source, const std::string& chunkHeaderID, int startIndex, Endianness endianness = Endianness::LittleEndian); + static inline AudioFileFormat determineAudioFileFormat (const std::vector& fileData); + + static inline int32_t fourBytesToInt (const std::vector& source, int startIndex, Endianness endianness = Endianness::LittleEndian); + static inline int16_t twoBytesToInt (const std::vector& source, int startIndex, Endianness endianness = Endianness::LittleEndian); + static inline int getIndexOfString (const std::vector& source, std::string s); + static inline int getIndexOfChunk (const std::vector& source, const std::string& chunkHeaderID, int startIndex, Endianness endianness = Endianness::LittleEndian); //============================================================= - uint32_t getAiffSampleRate (std::vector& fileData, int sampleRateStartIndex); - bool tenByteMatch (std::vector& v1, int startIndex1, std::vector& v2, int startIndex2); - void addSampleRateToAiffData (std::vector& fileData, uint32_t sampleRate); + static inline uint32_t getAiffSampleRate (const std::vector& fileData, int sampleRateStartIndex); + static inline void addSampleRateToAiffData (std::vector& fileData, uint32_t sampleRate); //============================================================= - void addStringToFileData (std::vector& fileData, std::string s); - void addInt32ToFileData (std::vector& fileData, int32_t i, Endianness endianness = Endianness::LittleEndian); - void addInt16ToFileData (std::vector& fileData, int16_t i, Endianness endianness = Endianness::LittleEndian); + static inline void addStringToFileData (std::vector& fileData, std::string s); + static inline void addInt32ToFileData (std::vector& fileData, int32_t i, Endianness endianness = Endianness::LittleEndian); + static inline void addInt16ToFileData (std::vector& fileData, int16_t i, Endianness endianness = Endianness::LittleEndian); //============================================================= - bool writeDataToFile (std::vector& fileData, std::string filePath); + static inline bool writeDataToFile (const std::vector& fileData, std::string filePath); //============================================================= - void reportError (std::string errorMessage); + void reportError (const std::string& errorMessage); //============================================================= AudioFileFormat audioFileFormat; @@ -270,27 +273,14 @@ struct AudioSampleConverter }; //============================================================= -// Pre-defined 10-byte representations of common sample rates -static std::unordered_map > aiffSampleRateTable = { - {8000, {64, 11, 250, 0, 0, 0, 0, 0, 0, 0}}, - {11025, {64, 12, 172, 68, 0, 0, 0, 0, 0, 0}}, - {16000, {64, 12, 250, 0, 0, 0, 0, 0, 0, 0}}, - {22050, {64, 13, 172, 68, 0, 0, 0, 0, 0, 0}}, - {32000, {64, 13, 250, 0, 0, 0, 0, 0, 0, 0}}, - {37800, {64, 14, 147, 168, 0, 0, 0, 0, 0, 0}}, - {44056, {64, 14, 172, 24, 0, 0, 0, 0, 0, 0}}, - {44100, {64, 14, 172, 68, 0, 0, 0, 0, 0, 0}}, - {47250, {64, 14, 184, 146, 0, 0, 0, 0, 0, 0}}, - {48000, {64, 14, 187, 128, 0, 0, 0, 0, 0, 0}}, - {50000, {64, 14, 195, 80, 0, 0, 0, 0, 0, 0}}, - {50400, {64, 14, 196, 224, 0, 0, 0, 0, 0, 0}}, - {88200, {64, 15, 172, 68, 0, 0, 0, 0, 0, 0}}, - {96000, {64, 15, 187, 128, 0, 0, 0, 0, 0, 0}}, - {176400, {64, 16, 172, 68, 0, 0, 0, 0, 0, 0}}, - {192000, {64, 16, 187, 128, 0, 0, 0, 0, 0, 0}}, - {352800, {64, 17, 172, 68, 0, 0, 0, 0, 0, 0}}, - {2822400, {64, 20, 172, 68, 0, 0, 0, 0, 0, 0}}, - {5644800, {64, 21, 172, 68, 0, 0, 0, 0, 0, 0}} +struct AiffUtilities +{ + //============================================================= + /** Decode an 80-bit (10 byte) sample rate to a double */ + static inline double decodeAiffSampleRate (const uint8_t* bytes); + + /** Encode a double as an 80-bit (10-byte) sample rate */ + static inline void encodeAiffSampleRate (double sampleRate, uint8_t* bytes); }; //============================================================= @@ -328,7 +318,7 @@ AudioFile::AudioFile() //============================================================= template -AudioFile::AudioFile (std::string filePath) +AudioFile::AudioFile (const std::string& filePath) : AudioFile() { load (filePath); @@ -390,18 +380,18 @@ double AudioFile::getLengthInSeconds() const template void AudioFile::printSummary() const { - std::cout << "|======================================|" << std::endl; - std::cout << "Num Channels: " << getNumChannels() << std::endl; - std::cout << "Num Samples Per Channel: " << getNumSamplesPerChannel() << std::endl; - std::cout << "Sample Rate: " << sampleRate << std::endl; - std::cout << "Bit Depth: " << bitDepth << std::endl; - std::cout << "Length in Seconds: " << getLengthInSeconds() << std::endl; - std::cout << "|======================================|" << std::endl; + std::cerr << "|======================================|" << std::endl; + std::cerr << "Num Channels: " << getNumChannels() << std::endl; + std::cerr << "Num Samples Per Channel: " << getNumSamplesPerChannel() << std::endl; + std::cerr << "Sample Rate: " << sampleRate << std::endl; + std::cerr << "Bit Depth: " << bitDepth << std::endl; + std::cerr << "Length in Seconds: " << getLengthInSeconds() << std::endl; + std::cerr << "|======================================|" << std::endl; } //============================================================= template -bool AudioFile::setAudioBuffer (AudioBuffer& newBuffer) +bool AudioFile::setAudioBuffer (const AudioBuffer& newBuffer) { int numChannels = (int)newBuffer.size(); @@ -499,7 +489,7 @@ void AudioFile::shouldLogErrorsToConsole (bool logErrors) //============================================================= template -bool AudioFile::load (std::string filePath) +bool AudioFile::load (const std::string& filePath) { std::ifstream file (filePath, std::ios::binary); @@ -545,7 +535,7 @@ bool AudioFile::load (std::string filePath) //============================================================= template -bool AudioFile::loadFromMemory (std::vector& fileData) +bool AudioFile::loadFromMemory (const std::vector& fileData) { // get audio file format audioFileFormat = determineAudioFileFormat (fileData); @@ -567,7 +557,7 @@ bool AudioFile::loadFromMemory (std::vector& fileData) //============================================================= template -bool AudioFile::decodeWaveFile (std::vector& fileData) +bool AudioFile::decodeWaveFile (const std::vector& fileData) { // ----------------------------------------------------------- // HEADER CHUNK @@ -726,7 +716,7 @@ bool AudioFile::decodeWaveFile (std::vector& fileData) //============================================================= template -bool AudioFile::decodeAiffFile (std::vector& fileData) +bool AudioFile::decodeAiffFile (const std::vector& fileData) { // ----------------------------------------------------------- // HEADER CHUNK @@ -881,44 +871,24 @@ bool AudioFile::decodeAiffFile (std::vector& fileData) //============================================================= template -uint32_t AudioFile::getAiffSampleRate (std::vector& fileData, int sampleRateStartIndex) +uint32_t AudioFile::getAiffSampleRate (const std::vector& fileData, int sampleRateStartIndex) { - for (auto it : aiffSampleRateTable) - { - if (tenByteMatch (fileData, sampleRateStartIndex, it.second, 0)) - return it.first; - } - - return 0; + double sampleRate = AiffUtilities::decodeAiffSampleRate (&fileData[sampleRateStartIndex]); + return static_cast (sampleRate); } //============================================================= template -bool AudioFile::tenByteMatch (std::vector& v1, int startIndex1, std::vector& v2, int startIndex2) +void AudioFile::addSampleRateToAiffData (std::vector& fileData, uint32_t sampleRateToAdd) { - for (int i = 0; i < 10; i++) - { - if (v1[startIndex1 + i] != v2[startIndex2 + i]) - return false; - } - - return true; + std::array sampleRateData; + AiffUtilities::encodeAiffSampleRate (static_cast (sampleRateToAdd), sampleRateData.data()); + fileData.insert (fileData.end(), sampleRateData.begin(), sampleRateData.end()); } //============================================================= template -void AudioFile::addSampleRateToAiffData (std::vector& fileData, uint32_t sampleRate) -{ - if (aiffSampleRateTable.count (sampleRate) > 0) - { - for (int i = 0; i < 10; i++) - fileData.push_back (aiffSampleRateTable[sampleRate][i]); - } -} - -//============================================================= -template -bool AudioFile::save (std::string filePath, AudioFileFormat format) +bool AudioFile::save (const std::string& filePath, AudioFileFormat format) { if (format == AudioFileFormat::Wave) { @@ -934,7 +904,7 @@ bool AudioFile::save (std::string filePath, AudioFileFormat format) //============================================================= template -bool AudioFile::saveToWaveFile (std::string filePath) +bool AudioFile::saveToWaveFile (const std::string& filePath) { std::vector fileData; @@ -1051,7 +1021,7 @@ bool AudioFile::saveToWaveFile (std::string filePath) //============================================================= template -bool AudioFile::saveToAiffFile (std::string filePath) +bool AudioFile::saveToAiffFile (const std::string& filePath) { std::vector fileData; @@ -1156,24 +1126,18 @@ bool AudioFile::saveToAiffFile (std::string filePath) //============================================================= template -bool AudioFile::writeDataToFile (std::vector& fileData, std::string filePath) +bool AudioFile::writeDataToFile (const std::vector& fileData, std::string filePath) { std::ofstream outputFile (filePath, std::ios::binary); - - if (outputFile.is_open()) + + if (!outputFile.is_open()) { - for (size_t i = 0; i < fileData.size(); i++) - { - char value = (char) fileData[i]; - outputFile.write (&value, sizeof (char)); - } - - outputFile.close(); - - return true; + return false; } - - return false; + + outputFile.write ((const char*)fileData.data(), fileData.size()); + outputFile.close(); + return true; } //============================================================= @@ -1205,8 +1169,8 @@ void AudioFile::addInt32ToFileData (std::vector& fileData, int32_t i bytes[3] = i & 0xFF; } - for (int i = 0; i < 4; i++) - fileData.push_back (bytes[i]); + for (int j = 0; j < 4; j++) + fileData.push_back (bytes[j]); } //============================================================= @@ -1244,8 +1208,11 @@ void AudioFile::clearAudioBuffer() //============================================================= template -AudioFileFormat AudioFile::determineAudioFileFormat (std::vector& fileData) +AudioFileFormat AudioFile::determineAudioFileFormat (const std::vector& fileData) { + if (fileData.size() < 4) + return AudioFileFormat::Error; + std::string header (fileData.begin(), fileData.begin() + 4); if (header == "RIFF") @@ -1258,7 +1225,7 @@ AudioFileFormat AudioFile::determineAudioFileFormat (std::vector& fi //============================================================= template -int32_t AudioFile::fourBytesToInt (std::vector& source, int startIndex, Endianness endianness) +int32_t AudioFile::fourBytesToInt (const std::vector& source, int startIndex, Endianness endianness) { if (source.size() >= (startIndex + 4)) { @@ -1280,7 +1247,7 @@ int32_t AudioFile::fourBytesToInt (std::vector& source, int startInd //============================================================= template -int16_t AudioFile::twoBytesToInt (std::vector& source, int startIndex, Endianness endianness) +int16_t AudioFile::twoBytesToInt (const std::vector& source, int startIndex, Endianness endianness) { int16_t result; @@ -1294,28 +1261,7 @@ int16_t AudioFile::twoBytesToInt (std::vector& source, int startInde //============================================================= template -int AudioFile::getIndexOfString (std::vector& source, std::string stringToSearchFor) -{ - int index = -1; - int stringLength = (int)stringToSearchFor.length(); - - for (size_t i = 0; i < source.size() - stringLength;i++) - { - std::string section (source.begin() + i, source.begin() + i + stringLength); - - if (section == stringToSearchFor) - { - index = static_cast (i); - break; - } - } - - return index; -} - -//============================================================= -template -int AudioFile::getIndexOfChunk (std::vector& source, const std::string& chunkHeaderID, int startIndex, Endianness endianness) +int AudioFile::getIndexOfChunk (const std::vector& source, const std::string& chunkHeaderID, int startIndex, Endianness endianness) { constexpr int dataLen = 4; @@ -1339,7 +1285,13 @@ int AudioFile::getIndexOfChunk (std::vector& source, const std::stri if ((i + 4) >= source.size()) return -1; - auto chunkSize = fourBytesToInt (source, i, endianness); + int32_t chunkSize = fourBytesToInt (source, i, endianness); + // Assume chunk size is invalid if it's greater than the number of bytes remaining in source + if (chunkSize > (source.size() - i - dataLen) || (chunkSize < 0)) + { + assert (false && "Invalid chunk size"); + return -1; + } i += (dataLen + chunkSize); } @@ -1348,10 +1300,10 @@ int AudioFile::getIndexOfChunk (std::vector& source, const std::stri //============================================================= template -void AudioFile::reportError (std::string errorMessage) +void AudioFile::reportError (const std::string& errorMessage) { if (logErrorsToConsole) - std::cout << errorMessage << std::endl; + std::cerr << errorMessage << std::endl; } //============================================================= @@ -1578,6 +1530,82 @@ T AudioSampleConverter::clamp (T value, T minValue, T maxValue) return value; } +//============================================================= +inline double AiffUtilities::decodeAiffSampleRate (const uint8_t* bytes) +{ + // Note: Sample rate is 80 bits made up of + // * 1 sign bit + // * 15 exponent bits + // * 64 mantissa bits + + // ---------------------------------------------- + // Sign + + // Extract the sign (most significant bit of byte 0) + int sign = (bytes[0] & 0x80) ? -1 : 1; + + // ---------------------------------------------- + // Exponent + + // byte 0: ignore the sign and shift the most significant bits to the left by one byte + uint16_t msbShifted = (static_cast (bytes[0] & 0x7F) << 8); + + // calculate exponent by combining byte 0 and byte 1 and subtract bias + uint16_t exponent = (msbShifted | static_cast (bytes[1])) - 16383; + + // ---------------------------------------------- + // Mantissa + + // Extract the mantissa (remaining 64 bits) by looping over the remaining + // bytes and combining them while shifting the result to the left by + // 8 bits each time + uint64_t mantissa = 0; + + for (int i = 2; i < 10; ++i) + mantissa = (mantissa << 8) | bytes[i]; + + // Normalize the mantissa (implicit leading 1 for normalized values) + double normalisedMantissa = static_cast (mantissa) / (1ULL << 63); + + // ---------------------------------------------- + // Combine sign, exponent, and mantissa into a double + + return sign * std::ldexp (normalisedMantissa, exponent); +} + +//============================================================= +inline void AiffUtilities::encodeAiffSampleRate (double sampleRate, uint8_t* bytes) +{ + // Determine the sign + int sign = (sampleRate < 0) ? -1 : 1; + + if (sign == -1) + sampleRate = -sampleRate; + + // Set most significant bit of byte 0 for the sign + bytes[0] = (sign == -1) ? 0x80 : 0x00; + + // Calculate the exponent using logarithm (log base 2) + int exponent = (log (sampleRate) / log (2.0)); + + // Add bias to exponent for AIFF + uint16_t biasedExponent = static_cast (exponent + 16383); + + // Normalize the sample rate + double normalizedSampleRate = sampleRate / pow (2.0, exponent); + + // Calculate the mantissa + uint64_t mantissa = static_cast (normalizedSampleRate * (1ULL << 63)); + + // Pack the exponent into first two bytes of 10-byte AIFF format + bytes[0] |= (biasedExponent >> 8) & 0x7F; // Upper 7 bits of exponent + bytes[1] = biasedExponent & 0xFF; // Lower 8 bits of exponent + + // Put the mantissa into byte array + for (int i = 0; i < 8; ++i) + bytes[2 + i] = (mantissa >> (8 * (7 - i))) & 0xFF; +} + #if defined (_MSC_VER) __pragma(warning (pop)) #elif defined (__GNUC__) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4003e08..b2e4751 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ #=============================================================================== cmake_minimum_required (VERSION 3.12) -project ("AudioFile" VERSION 1.1.1 +project ("AudioFile" VERSION 1.1.2 DESCRIPTION "A simple C++ library for reading and writing audio files." HOMEPAGE_URL "https://github.com/adamstark/AudioFile") @@ -13,19 +13,15 @@ option (BUILD_TESTS "Build tests" ON) option (BUILD_EXAMPLES "Build examples" ON) #=============================================================================== -add_library (${PROJECT_NAME} INTERFACE) +set (AUDIOFILE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/AudioFile.h) +source_group (TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${AUDIOFILE_SOURCES}) -#=============================================================================== -if(MSVC) - # needed for M_PI macro - add_definitions(-D_USE_MATH_DEFINES) -endif() +add_library (${PROJECT_NAME} INTERFACE ${AUDIOFILE_SOURCES}) #=============================================================================== -target_include_directories ( - ${PROJECT_NAME} - INTERFACE $ - $) +target_include_directories (${PROJECT_NAME} INTERFACE + $ + $) #=============================================================================== target_compile_features (${PROJECT_NAME} INTERFACE cxx_std_17) @@ -42,3 +38,48 @@ endif () #=============================================================================== set (CMAKE_SUPPRESS_REGENERATION true) + +#=============================================================================== +include (CMakePackageConfigHelpers) + +install (TARGETS ${PROJECT_NAME} + EXPORT ${PROJECT_NAME}Targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +install (FILES ${PROJECT_SOURCE_DIR}/AudioFile.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} +) + +install (EXPORT ${PROJECT_NAME}Targets + FILE ${PROJECT_NAME}Targets.cmake + NAMESPACE ${PROJECT_NAME}:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} +) + +file (WRITE ${PROJECT_BINARY_DIR}/Config.cmake.in + "@PACKAGE_INIT@\n\ninclude(\"\${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake\")\n\ncheck_required_components(@PROJECT_NAME@)\n" +) + +configure_package_config_file ( + ${PROJECT_BINARY_DIR}/Config.cmake.in + ${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake + INSTALL_DESTINATION + ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} +) + +write_basic_package_version_file ( + ${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion + ARCH_INDEPENDENT +) + +install (FILES + ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} +) diff --git a/README.md b/README.md index f6eec4e..6ba20da 100644 --- a/README.md +++ b/README.md @@ -1,189 +1,189 @@ # AudioFile -![Version](https://img.shields.io/badge/version-1.1.1-green.svg?style=flat-square) -![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square) -![Language](https://img.shields.io/badge/language-C++-yellow.svg?style=flat-square) -A simple header-only C++ library for reading and writing audio files. +![Version](https://img.shields.io/badge/version-1.1.2-green.svg?style=flat-square) +![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square) +![Language](https://img.shields.io/badge/language-C++-yellow.svg?style=flat-square) + +A simple header-only C++ library for reading and writing audio files. Current supported formats: -* WAV -* AIFF +- WAV +- AIFF -Author ------- +## Author AudioFile is written and maintained by Adam Stark. [http://www.adamstark.co.uk](http://www.adamstark.co.uk) -Usage ------ +## Usage ### Create an AudioFile object: - #include "AudioFile.h" + #include "AudioFile.h" + + AudioFile audioFile; - AudioFile audioFile; - ### Load an audio file: - audioFile.load ("/path/to/my/audiofile.wav"); - + audioFile.load ("/path/to/my/audiofile.wav"); + ### Get some information about the loaded audio: - int sampleRate = audioFile.getSampleRate(); - int bitDepth = audioFile.getBitDepth(); - - int numSamples = audioFile.getNumSamplesPerChannel(); - double lengthInSeconds = audioFile.getLengthInSeconds(); - - int numChannels = audioFile.getNumChannels(); - bool isMono = audioFile.isMono(); - bool isStereo = audioFile.isStereo(); - - // or, just use this quick shortcut to print a summary to the console - audioFile.printSummary(); - + int sampleRate = audioFile.getSampleRate(); + int bitDepth = audioFile.getBitDepth(); + + int numSamples = audioFile.getNumSamplesPerChannel(); + double lengthInSeconds = audioFile.getLengthInSeconds(); + + int numChannels = audioFile.getNumChannels(); + bool isMono = audioFile.isMono(); + bool isStereo = audioFile.isStereo(); + + // or, just use this quick shortcut to print a summary to the console + audioFile.printSummary(); + ### Access the samples directly: - int channel = 0; - int numSamples = audioFile.getNumSamplesPerChannel(); + int channel = 0; + int numSamples = audioFile.getNumSamplesPerChannel(); - for (int i = 0; i < numSamples; i++) - { - double currentSample = audioFile.samples[channel][i]; - } + for (int i = 0; i < numSamples; i++) + { + double currentSample = audioFile.samples[channel][i]; + } ### Replace the AudioFile audio buffer with another - // 1. Create an AudioBuffer - // (BTW, AudioBuffer is just a vector of vectors) - - AudioFile::AudioBuffer buffer; - - // 2. Set to (e.g.) two channels - buffer.resize (2); - - // 3. Set number of samples per channel - buffer[0].resize (100000); - buffer[1].resize (100000); - - // 4. do something here to fill the buffer with samples, e.g. - - #include // somewhere earler (for M_PI and sinf()) - - // then... - - int numChannels = 2; - int numSamplesPerChannel = 100000; - float sampleRate = 44100.f; - float frequency = 440.f; - - for (int i = 0; i < numSamplesPerChannel; i++) - { + // 1. Create an AudioBuffer + // (BTW, AudioBuffer is just a vector of vectors) + + AudioFile::AudioBuffer buffer; + + // 2. Set to (e.g.) two channels + buffer.resize (2); + + // 3. Set number of samples per channel + buffer[0].resize (100000); + buffer[1].resize (100000); + + // 4. do something here to fill the buffer with samples, e.g. + + #include // somewhere earler (for M_PI and sinf()) + + // then... + + int numChannels = 2; + int numSamplesPerChannel = 100000; + float sampleRate = 44100.f; + float frequency = 440.f; + + for (int i = 0; i < numSamplesPerChannel; i++) + { float sample = sinf (2. * M_PI * ((float) i / sampleRate) * frequency) ; - + for (int channel = 0; channel < numChannels; channel++) buffer[channel][i] = sample * 0.5; - } - - // 5. Put into the AudioFile object - bool ok = audioFile.setAudioBuffer (buffer); - - -### Resize the audio buffer - - // Set both the number of channels and number of samples per channel - audioFile.setAudioBufferSize (numChannels, numSamples); - - // Set the number of samples per channel - audioFile.setNumSamplesPerChannel (numSamples); - - // Set the number of channels - audioFile.setNumChannels (numChannels); - + } + + // 5. Put into the AudioFile object + bool ok = audioFile.setAudioBuffer (buffer); + +### Resize the audio buffer + + // Set both the number of channels and number of samples per channel + audioFile.setAudioBufferSize (numChannels, numSamples); + + // Set the number of samples per channel + audioFile.setNumSamplesPerChannel (numSamples); + + // Set the number of channels + audioFile.setNumChannels (numChannels); + ### Set bit depth and sample rate - - audioFile.setBitDepth (24); - audioFile.setSampleRate (44100); - + + audioFile.setBitDepth (24); + audioFile.setSampleRate (44100); + ### Save the audio file to disk - - // Wave file (implicit) - audioFile.save ("path/to/desired/audioFile.wav"); - - // Wave file (explicit) - audioFile.save ("path/to/desired/audioFile.wav", AudioFileFormat::Wave); - - // Aiff file - audioFile.save ("path/to/desired/audioFile.aif", AudioFileFormat::Aiff); + // Wave file (implicit) + audioFile.save ("path/to/desired/audioFile.wav"); -Examples ------------------ + // Wave file (explicit) + audioFile.save ("path/to/desired/audioFile.wav", AudioFileFormat::Wave); -Please see the `examples` folder for some examples on library usage. + // Aiff file + audioFile.save ("path/to/desired/audioFile.aif", AudioFileFormat::Aiff); +## Examples -A Note On Types ------------------ +Please see the `examples` folder for some examples on library usage. -AudioFile is a template class and so it can be instantiated using floating point precision: +## A Note On Types -For example +AudioFile is a template class and so it can be instantiated using different types to represent the audio samples. - AudioFile audioFile; +For example, we can use floating point precision... + + AudioFile audioFile; ...or double precision... - AudioFile audioFile; - + AudioFile audioFile; + ...or an integer type: - AudioFile audioFile; - -This simply reflects the data type you would like to use to store the underlying audio samples. + AudioFile audioFile; -When you use an integer type to store the samples (e.g. `int` or `int8_t` or `int16_t` or `uint32_t`), the library will read in the integer sample values directly from the audio file. A couple of notes on integer types: +This simply reflects the data type you would like to use to store the underlying audio samples. -* The range of samples is designed to be symmetric. This means that for (e.g.) an signed 8-bit integer (`int8_t`) we will use the range `[-127, 127]` for storing samples representing the `[-1., 1.]` range. The value `-128` is possible here given the `int8_t` type, but this is interpreted as a value slightly lower than `-1` (specifically `-1.007874015748`). +When you use an integer type to store the samples (e.g. `int` or `int8_t` or `int16_t` or `uint32_t`), the library will read in the integer sample values directly from the audio file. -* In the case of unsigned types, we obviously can't store samples as negative values. Therefore, we used the equivalent range of the unsigned type in use. E.g. if with a 8-bit signed integer (`int8_t`) the range would be `[-127, 127]`, for an 8-bit unsigned integer we would use the range `[1, 255]`. Note that we don't use `-128` for `int8_t` or `0` in `uint8_t`. +A couple of notes on integer types: -* If you try to read an audio file with a larger bit-depth than the type you are using to store samples, the attempt to read the file will fail. Put more simply, you can't read a 16-bit audio file into an 8-bit integer. +- The range of samples is designed to be symmetric. This means that for (e.g.) an signed 8-bit integer (`int8_t`) we will use the range `[-127, 127]` for storing samples representing the `[-1., 1.]` range. The value `-128` is possible here given the `int8_t` type, but this is interpreted as a value slightly lower than `-1` (specifically `-1.007874015748`). -* If you are writing audio samples in integer formats, you should use the correct sample range for both a) the type you are using to store samples; and b) the bit depth of the audio you want to write. +- In the case of unsigned types, we obviously can't store samples as negative values. Therefore, we used the equivalent range of the unsigned type in use. E.g. if with a 8-bit signed integer (`int8_t`) the range would be `[-127, 127]`, for an 8-bit unsigned integer we would use the range `[1, 255]`. Note that we don't use `-128` for `int8_t` or `0` in `uint8_t`. + +- If you try to read an audio file with a larger bit-depth than the type you are using to store samples, the attempt to read the file will fail. Put more simply, you can't read a 16-bit audio sample into an 8-bit integer. + +- If you are writing audio samples in integer formats, you should use the correct sample range for both a) the type you are using to store samples; and b) the bit depth of the audio you want to write. The following table details the sample range for each bit-depth: -| Type | 8-bit Audio | 16-bit Audio | 24-bit Audio | 32-bit Audio | -| ------------- | ------------- | ------------- | ------------- | ------------- | -| `float` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | -| `double` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | -| `int8_t` | `[-127, 127]` | :x: (type too small) | :x: (type too small) | :x: (type too small) | -| `uint8_t` | `[1, 255]` | :x: (type too small) | :x: (type too small) | :x: (type too small) | -| `int16_t` | `[-127, 127]` | `[-32767, 32767]` | :x: (type too small) | :x: (type too small) | -| `uint16_t` | `[1, 255]` | `[1, 65535]` | :x: (type too small) | :x: (type too small) | -| `int32_t` | `[-127, 127]` | `[-32767, 32767]` | [`-8388607, 8388607]` | `[-2147483647, 2147483647]` | -| `uint32_t` | `[1, 255]` | `[1, 65535]` | `[1, 16777215]` | `[1, 4294967295]` | -| `int64_t` | `[-127, 127]` | `[-32767, 32767]` | [`-8388607, 8388607]` | `[-2147483647, 2147483647]` | -| `uint64_t` | `[1, 255]` | `[1, 65535]` | `[1, 16777215]` | `[1, 4294967295]` | +| Type | 8-bit Audio | 16-bit Audio | 24-bit Audio | 32-bit Audio | +| ---------- | ------------- | -------------------- | --------------------- | --------------------------- | +| `float` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | +| `double` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | `[-1.0, 1.0]` | +| `int8_t` | `[-127, 127]` | :x: (type too small) | :x: (type too small) | :x: (type too small) | +| `uint8_t` | `[1, 255]` | :x: (type too small) | :x: (type too small) | :x: (type too small) | +| `int16_t` | `[-127, 127]` | `[-32767, 32767]` | :x: (type too small) | :x: (type too small) | +| `uint16_t` | `[1, 255]` | `[1, 65535]` | :x: (type too small) | :x: (type too small) | +| `int32_t` | `[-127, 127]` | `[-32767, 32767]` | [`-8388607, 8388607]` | `[-2147483647, 2147483647]` | +| `uint32_t` | `[1, 255]` | `[1, 65535]` | `[1, 16777215]` | `[1, 4294967295]` | +| `int64_t` | `[-127, 127]` | `[-32767, 32767]` | [`-8388607, 8388607]` | `[-2147483647, 2147483647]` | +| `uint64_t` | `[1, 255]` | `[1, 65535]` | `[1, 16777215]` | `[1, 4294967295]` | -Error Messages ------------------ +## Error Messages -By default, the library logs error messages to the console to provide information on what has gone wrong (e.g. a file we tried to load didn't exist). +By default, the library logs error messages to the console to provide information on what has gone wrong (e.g. a file we tried to load didn't exist). If you prefer not to see these messages, you can disable this error logging behaviour using: - audioFile.shouldLogErrorsToConsole (false); + audioFile.shouldLogErrorsToConsole (false); + +## Versions +##### 1.1.2 - 18th November 2024 -Versions -------- +- Improved AIFF sample rate calculations +- Improved CMake support +- Code improvements +- Bug and warning fixes ##### 1.1.1 - 4th April 2023 @@ -217,7 +217,7 @@ Versions ##### 1.0.6 - 29th February 2020 - Made error logging to the console optional -- Fixed lots of compiler warnings +- Fixed lots of compiler warnings ##### 1.0.5 - 14th October 2019 @@ -237,36 +237,39 @@ Versions - Bug fixes -Contributions -------- +## Contributions Many thanks to the following people for their contributions to this library: -* [Abhinav1997](https://github.com/Abhinav1997) -* [alxarsenault](https://github.com/alxarsenault) -* [BenjaminHinchliff](https://github.com/BenjaminHinchliff) -* [emiro85](https://github.com/emiro85) -* [heartofrain](https://github.com/heartofrain) -* [helloimmatt](https://github.com/helloimmatt/) -* [MatthieuHernandez](https://github.com/MatthieuHernandez) -* [mrpossoms](https://github.com/mrpossoms) -* [mynameisjohn](https://github.com/mynameisjohn) -* [Sidelobe](https://github.com/Sidelobe) -* [sschaetz](https://github.com/sschaetz) -* [Yhcrown](https://github.com/Yhcrown) - -Want to Contribute? -------- +- [Abhinav1997](https://github.com/Abhinav1997) +- [alxarsenault](https://github.com/alxarsenault) +- [ascii255](https://github.com/ascii255) +- [BenjaminHinchliff](https://github.com/BenjaminHinchliff) +- [BesselJ](https://github.com/BesselJ) +- [cgraf78](https://github.com/cgraf78) +- [emiro85](https://github.com/emiro85) +- [encoded](https://github.com/encoded) +- [heartofrain](https://github.com/heartofrain) +- [helloimmatt](https://github.com/helloimmatt/) +- [leocstone](https://github.com/leocstone) +- [MatthieuHernandez](https://github.com/MatthieuHernandez) +- [Metalsofa](https://github.com/Metalsofa) +- [mrpossoms](https://github.com/mrpossoms) +- [mynameisjohn](https://github.com/mynameisjohn) +- [Sidelobe](https://github.com/Sidelobe) +- [sschaetz](https://github.com/sschaetz) +- [Yhcrown](https://github.com/Yhcrown) + +## Want to Contribute? If you would like to submit a pull request for this library, please do! But kindly follow the following simple guidelines... -* Make the changes as concise as is possible for the change you are proposing -* Avoid unnecessarily changing a large number of lines - e.g. commits changing the number of spaces in indentations on all lines (and so on) -* Keep to the code style of this library which is the [JUCE Coding Standards](https://juce.com/discover/stories/coding-standards) -* Make the changes relative to the develop branch of the library (as this may have advanced beyond the master branch) +- Make the changes as concise as is possible for the change you are proposing +- Avoid unnecessarily changing a large number of lines - e.g. commits changing the number of spaces in indentations on all lines (and so on) +- Keep to the code style of this library which is the [JUCE Coding Standards](https://juce.com/discover/stories/coding-standards) +- Make the changes relative to the develop branch of the library (as this may have advanced beyond the master branch) -License -------- +## License MIT License diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 58ed174..6d16333 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,7 +1,12 @@ -include_directories (${AudioFile_SOURCE_DIR}) - -add_definitions (-DPROJECT_BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}") file (COPY test-audio.wav DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) -add_executable (Examples examples.cpp) -target_link_libraries (Examples AudioFile) +set (AUDIOFILE_EXAMPLE Example) + +file (GLOB AUDIOFILE_EXAMPLES_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +source_group (TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${AUDIOFILE_EXAMPLES_SOURCES}) + +add_executable (${AUDIOFILE_EXAMPLE} ${AUDIOFILE_EXAMPLES_SOURCES}) +target_link_libraries (${AUDIOFILE_EXAMPLE} PUBLIC AudioFile) +target_compile_definitions (${AUDIOFILE_EXAMPLE} PUBLIC + -D_USE_MATH_DEFINES # needed for M_PI macro + -DPROJECT_BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}") \ No newline at end of file diff --git a/examples/examples.cpp b/examples/examples.cpp index 6662839..7823520 100644 --- a/examples/examples.cpp +++ b/examples/examples.cpp @@ -141,6 +141,6 @@ namespace examples // 4. Write audio file to disk std::string outputFilePath = "quieter-audio-file.wav"; // change this to somewhere useful for you - a.save (outputFilePath, AudioFileFormat::Aiff); + a.save (outputFilePath, AudioFileFormat::Wave); } } diff --git a/tests/AiffLoadingTests.cpp b/tests/AiffLoadingTests.cpp index 90f71c9..d29909e 100644 --- a/tests/AiffLoadingTests.cpp +++ b/tests/AiffLoadingTests.cpp @@ -1,4 +1,4 @@ -#include "doctest.h" +#include "doctest/doctest.h" #include #include #include @@ -350,3 +350,66 @@ TEST_SUITE ("AiffLoadingTests - Integer Types - 24-bit File") test24Bit44100WithInteger (true); } } + +//============================================================= +TEST_SUITE ("AiffLoadingTests - Sample Rates") +{ + //============================================================= + TEST_CASE ("AiffLoadingTests - Sample Rates - Common Sample Rates") + { + // Pre-defined 10-byte representations of common sample rates + std::unordered_map > aiffSampleRateTable = { + {8000, {64, 11, 250, 0, 0, 0, 0, 0, 0, 0}}, + {11025, {64, 12, 172, 68, 0, 0, 0, 0, 0, 0}}, + {16000, {64, 12, 250, 0, 0, 0, 0, 0, 0, 0}}, + {22050, {64, 13, 172, 68, 0, 0, 0, 0, 0, 0}}, + {32000, {64, 13, 250, 0, 0, 0, 0, 0, 0, 0}}, + {37800, {64, 14, 147, 168, 0, 0, 0, 0, 0, 0}}, + {44056, {64, 14, 172, 24, 0, 0, 0, 0, 0, 0}}, + {44100, {64, 14, 172, 68, 0, 0, 0, 0, 0, 0}}, + {47250, {64, 14, 184, 146, 0, 0, 0, 0, 0, 0}}, + {48000, {64, 14, 187, 128, 0, 0, 0, 0, 0, 0}}, + {50000, {64, 14, 195, 80, 0, 0, 0, 0, 0, 0}}, + {50400, {64, 14, 196, 224, 0, 0, 0, 0, 0, 0}}, + {88200, {64, 15, 172, 68, 0, 0, 0, 0, 0, 0}}, + {96000, {64, 15, 187, 128, 0, 0, 0, 0, 0, 0}}, + {176400, {64, 16, 172, 68, 0, 0, 0, 0, 0, 0}}, + {192000, {64, 16, 187, 128, 0, 0, 0, 0, 0, 0}}, + {352800, {64, 17, 172, 68, 0, 0, 0, 0, 0, 0}}, + {2822400, {64, 20, 172, 68, 0, 0, 0, 0, 0, 0}}, + {5644800, {64, 21, 172, 68, 0, 0, 0, 0, 0, 0}} + }; + + std::vector sampleRateBytes (10); + + for (const auto& pair : aiffSampleRateTable) + { + uint32_t sampleRate = pair.first; + double inputSampleRate = sampleRate; + + // encode into bytes + AiffUtilities::encodeAiffSampleRate (static_cast (sampleRate), sampleRateBytes.data()); + + for (int i = 0; i < 10; i++) + CHECK_EQ (sampleRateBytes[i], aiffSampleRateTable[sampleRate][i]); + + double outputSampleRate = AiffUtilities::decodeAiffSampleRate (aiffSampleRateTable[sampleRate].data()); + + CHECK_EQ (inputSampleRate, outputSampleRate); + } + } + + //============================================================= + TEST_CASE ("AiffLoadingTests - Sample Rates - Round Trip Encode/Decode Tests") + { + std::vector sampleRateBytes (10); + + for (int i = -100000; i < 100000; i += 237) // + odd number to reduce cycles but check odd and even values + { + double input = static_cast (i); + AiffUtilities::encodeAiffSampleRate (input, sampleRateBytes.data()); + double output = AiffUtilities::decodeAiffSampleRate (sampleRateBytes.data()); + CHECK_EQ (input, output); + } + } +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9007d3b..bfe8d6f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,15 +1,17 @@ -include_directories (doctest) -include_directories (${AudioFile_SOURCE_DIR}) - -add_definitions(-DPROJECT_BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}") - file (COPY test-audio DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) file (MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/audio-write-tests) -add_executable (Tests main.cpp GeneralTests.cpp WavLoadingTests.cpp AiffLoadingTests.cpp FileWritingTests.cpp SampleConversionTests.cpp) -target_compile_features (Tests PRIVATE cxx_std_17) -add_test (NAME Tests COMMAND Tests) +set (AUDIOFILE_TESTS Tests) + +file (GLOB AUDIOFILE_TESTS_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +source_group (TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${AUDIOFILE_TESTS_SOURCES}) + +add_executable (${AUDIOFILE_TESTS} ${AUDIOFILE_TESTS_SOURCES}) +target_include_directories (${AUDIOFILE_TESTS} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries (${AUDIOFILE_TESTS} PUBLIC AudioFile) +target_compile_features (${AUDIOFILE_TESTS} PRIVATE cxx_std_17) +target_compile_definitions (${AUDIOFILE_TESTS} PUBLIC + -D_USE_MATH_DEFINES # needed for M_PI macro + -DPROJECT_BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}") -# add_executable (LoadingTests main.cpp WavLoadingTests.cpp AiffLoadingTests.cpp) -# target_compile_features (LoadingTests PRIVATE cxx_std_17) -# add_test (NAME LoadingTests COMMAND LoadingTests) \ No newline at end of file +add_test (NAME ${AUDIOFILE_TESTS} COMMAND ${AUDIOFILE_TESTS}) \ No newline at end of file diff --git a/tests/FileWritingTests.cpp b/tests/FileWritingTests.cpp index b419a46..434d397 100644 --- a/tests/FileWritingTests.cpp +++ b/tests/FileWritingTests.cpp @@ -1,4 +1,4 @@ -#include "doctest.h" +#include "doctest/doctest.h" #include #include #include diff --git a/tests/GeneralTests.cpp b/tests/GeneralTests.cpp index cc0ff39..0fcfdaf 100644 --- a/tests/GeneralTests.cpp +++ b/tests/GeneralTests.cpp @@ -1,4 +1,4 @@ -#include "doctest.h" +#include "doctest/doctest.h" #include #include #include @@ -107,4 +107,13 @@ TEST_SUITE ("General Tests") checkFilesAreExactlyTheSame (a, b); } + + //============================================================= + TEST_CASE ("GeneralTests::Empty Data") + { + AudioFile a; + a.shouldLogErrorsToConsole (false); + bool result = a.loadFromMemory (std::vector()); + CHECK_EQ (result, false); + } } diff --git a/tests/SampleConversionTests.cpp b/tests/SampleConversionTests.cpp index 752fca1..b084f35 100644 --- a/tests/SampleConversionTests.cpp +++ b/tests/SampleConversionTests.cpp @@ -1,4 +1,4 @@ -#include "doctest.h" +#include "doctest/doctest.h" #include #include #include diff --git a/tests/WavLoadingTests.cpp b/tests/WavLoadingTests.cpp index 2735e9c..d0d216e 100644 --- a/tests/WavLoadingTests.cpp +++ b/tests/WavLoadingTests.cpp @@ -1,4 +1,4 @@ -#include "doctest.h" +#include "doctest/doctest.h" #include #include #include diff --git a/tests/main.cpp b/tests/main.cpp index 5b40db3..e2383eb 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -1,3 +1,3 @@ #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #define DOCTEST_CONFIG_COLORS_NONE -#include "doctest.h" +#include "doctest/doctest.h"