Skip to content

Commit

Permalink
Implemented JSON bitstream format
Browse files Browse the repository at this point in the history
  • Loading branch information
tornupnegatives committed Dec 5, 2022
1 parent afcd35d commit cb12f84
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 56 deletions.
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ project(TMS-Express)
# Import libsndfile and libsamplerate
include(cmake/FindSamplerate.cmake)
include(cmake/FindCLI11.cmake)
include(cmake/FindJSON.cmake)
include(test/TmsTest.cmake)

# Project files
Expand Down Expand Up @@ -38,11 +39,12 @@ set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME tmsexpress)
# Link dependencies
target_link_libraries(${PROJECT_NAME}
CLI11
nlohmann_json::nlohmann_json
${LIBSNDFILE_LIBRARY}
${LIBSAMPLERATE_LIBRARY}
)

# On macOS, supress warnings regarding <experimental/filesystem
# On macOS, suppress warnings regarding <experimental/filesystem
# On other systems, link the filesystem library
if (APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE _LIBCPP_NO_EXPERIMENTAL_DEPRECATION_WARNING_FILESYSTEM)
Expand All @@ -51,4 +53,3 @@ else()
stdc++fs
)
endif(APPLE)

3 changes: 1 addition & 2 deletions cmake/FindCLI11.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@ FetchContent_Declare(CLI11
GIT_REPOSITORY "https://github.com/CLIUtils/CLI11.git"
GIT_TAG "v2.2.0"
)

FetchContent_MakeAvailable(CLI11)

FetchContent_MakeAvailable(CLI11)
17 changes: 17 additions & 0 deletions cmake/FindJSON.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
###############################################################################
# Project: TMS-Express
#
# File: FindJSON.cmake
#
# Description: Imports nlohmann_json via GitHub
#
# Author: Joseph Bellahcen <[email protected]>
###############################################################################

include(FetchContent)

FetchContent_Declare(json
URL "https://github.com/nlohmann/json/releases/download/v3.11.2/json.tar.xz"
)

FetchContent_MakeAvailable(json)
4 changes: 2 additions & 2 deletions inc/Frame_Encoding/Frame.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#ifndef TMS_EXPRESS_FRAME_H
#define TMS_EXPRESS_FRAME_H

#include <nlohmann/json.hpp>
#include <string>
#include <vector>

Expand Down Expand Up @@ -35,9 +36,8 @@ class Frame {
bool isSilent() const;
bool isRepeat() const;

// The print() function remains implemented for debugging purposes
__attribute__((unused)) void print(int index = -1);
std::string toBinary();
nlohmann::json toJSON();

private:
int pitch;
Expand Down
6 changes: 4 additions & 2 deletions inc/Frame_Encoding/FrameEncoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@
class FrameEncoder {
public:
explicit FrameEncoder(bool hexPrefix = false, char separator = ',');
explicit FrameEncoder(const std::vector<Frame> &frames, bool hexPrefix = false, char separator = ',');
explicit FrameEncoder(const std::vector<Frame> &initFrames, bool hexPrefix = false, char separator = ',');

void appendFrame(Frame frame);
void appendFrames(const std::vector<Frame> &frames);
void appendFrames(const std::vector<Frame> &initFrames);
std::string toHex(bool shouldAppendStopFrame = true);
std::string toJSON();

private:
bool includeHexPrefix;
char byteSeparator;
std::vector<Frame> frames;
std::vector<std::string> bytes;

void appendStopFrame();
Expand Down
7 changes: 4 additions & 3 deletions inc/Interfaces/BitstreamGenerator.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
#ifndef TMS_EXPRESS_BITSTREAMGENERATOR_H
#define TMS_EXPRESS_BITSTREAMGENERATOR_H

#include "Frame_Encoding/Frame.h"
#include <string>

class BitstreamGenerator {
public:
typedef enum {ENCODERSTYLE_ASCII, ENCODERSTYLE_C, ENCODERSTYLE_ARDUINO} EncoderStyle;
typedef enum {ENCODERSTYLE_ASCII, ENCODERSTYLE_C, ENCODERSTYLE_ARDUINO, ENCODERSTYLE_JSON} EncoderStyle;
BitstreamGenerator(float windowMs, int highpassHz, int lowpassHz, float preemphasis, EncoderStyle style,
bool includeStopFrame, int gainShift, float maxVoicedDb, float maxUnvoicedDb, bool detectRepeats,
int maxHz, int minHz);
Expand All @@ -30,8 +31,8 @@ class BitstreamGenerator {
int maxHz;
int minHz;

std::string generateBitstream(const std::string &inputPath) const;
std::string formatBitstream(std::string bitstream, const std::string &filename);
std::vector<Frame> generateFrames(const std::string &inputPath) const;
std::string formatBitstream(const std::vector<Frame>& frames, const std::string &filename);
};

#endif //TMS_EXPRESS_BITSTREAMGENERATOR_H
40 changes: 19 additions & 21 deletions src/Frame_Encoding/Frame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@
#include "Frame_Encoding/Frame.h"
#include "Frame_Encoding/Tms5220CodingTable.h"
#include <algorithm>
#include <iostream>
#include <nlohmann/json.hpp>
#include <vector>

using namespace Tms5220CodingTable;
using std::cout;
using std::endl;

Frame::Frame(int pitchPeriod, bool isVoiced, float gainDB, std::vector<float> coeffs) {
pitch = pitchPeriod;
Expand Down Expand Up @@ -138,24 +136,6 @@ bool Frame::isRepeat() const {
// Frame Serialization
///////////////////////////////////////////////////////////////////////////////

__attribute__((unused)) void Frame::print(int index) {
// Frame header
cout << "Frame " << index << ":" << endl;

// Frame metadata
cout << "Pitch period (samples): " << pitch << endl;
cout << "Voicing: " << (voicedFrame ? "voiced" : "unvoiced") << endl;
cout << "Gain (dB): " << gain << endl;

cout << "Coeffs: [";
for (float coeff : reflectorCoeffs) {
cout << coeff << " ";
}

// Remove trailing space and "close" array
cout << "\b\b]" << endl << endl;
}

std::string Frame::toBinary() {
std::string bin;

Expand Down Expand Up @@ -193,6 +173,24 @@ std::string Frame::toBinary() {
return bin;
}

nlohmann::json Frame::toJSON() {
nlohmann::json jFrame;

// Raw values
jFrame["pitch"] = pitch;
jFrame["isVoiced"] = voicedFrame;
jFrame["isRepeat"] = repeatFrame;
jFrame["gain"] = gain;
jFrame["coeffs"] = nlohmann::json(reflectorCoeffs);

// Quantized values
jFrame["tms_pitch"] = getQuantizedPitchIdx();
jFrame["tms_gain"] = getQuantizedGainIdx();
jFrame["tms_coeffs"] = nlohmann::json(getQuantizedCoeffsIdx());

return jFrame;
}

///////////////////////////////////////////////////////////////////////////////
// Utility Functions
///////////////////////////////////////////////////////////////////////////////
Expand Down
23 changes: 19 additions & 4 deletions src/Frame_Encoding/FrameEncoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@
#include "Frame_Encoding/Frame.h"
#include <algorithm>
#include <cstdio>
#include <nlohmann/json.hpp>

FrameEncoder::FrameEncoder(bool hexPrefix, char separator) {
includeHexPrefix = hexPrefix;
byteSeparator = separator;
frames = std::vector<Frame>();
bytes = std::vector<std::string>(1, "");
}

FrameEncoder::FrameEncoder(const std::vector<Frame> &frames, bool hexPrefix, char separator) {
FrameEncoder::FrameEncoder(const std::vector<Frame> &initFrames, bool hexPrefix, char separator) {
includeHexPrefix = hexPrefix;
byteSeparator = separator;
frames = std::vector<Frame>();
bytes = std::vector<std::string>(1, "");

appendFrames(frames);
appendFrames(initFrames);
}

// Extract the binary representation of a Frame, segment it into bytes, and store the data
Expand All @@ -33,6 +36,7 @@ FrameEncoder::FrameEncoder(const std::vector<Frame> &frames, bool hexPrefix, cha
// may be packed into the empty space of an existing vector element, or the last few bits may partially occupy a new
// vector element
void FrameEncoder::appendFrame(Frame frame) {
frames.push_back(frame);
auto bin = frame.toBinary();

// Check to see if the previous byte is incomplete (contains less than 8 characters), and fill it if so
Expand All @@ -53,8 +57,8 @@ void FrameEncoder::appendFrame(Frame frame) {
}

// Extract the binary representation of an entire vector of Frames, and slice it into bytes
void FrameEncoder::appendFrames(const std::vector<Frame> &frames) {
for (const auto &frame: frames) {
void FrameEncoder::appendFrames(const std::vector<Frame> &initFrames) {
for (const auto &frame: initFrames) {
appendFrame(frame);
}
}
Expand Down Expand Up @@ -85,6 +89,17 @@ std::string FrameEncoder::toHex(bool shouldAppendStopFrame) {
return hexStream;
}

// Serialize the Frame data to a JSON object
std::string FrameEncoder::toJSON() {
nlohmann::json json;

for (auto frame : frames) {
json.push_back(frame.toJSON());
}

return json.dump(4);
}

// End the hex stream with a stop frame, which signifies to the TMS5220 that the Speak External command has completed
// and the device should halt execution
void FrameEncoder::appendStopFrame() {
Expand Down
42 changes: 23 additions & 19 deletions src/Interfaces/BitstreamGenerator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
#include <experimental/filesystem>
#include <fstream>
#include <iostream>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>

namespace fs = std::experimental::filesystem;

BitstreamGenerator::BitstreamGenerator(float windowMs, int highpassHz, int lowpassHz, float preemphasis, EncoderStyle style,
bool includeStopFrame, int gainShift, float maxVoicedDb, float maxUnvoicedDb, bool detectRepeats,
int maxHz, int minHz) : windowMs(windowMs), highpassHz(highpassHz), lowpassHz(lowpassHz),
Expand All @@ -36,8 +36,8 @@ BitstreamGenerator::BitstreamGenerator(float windowMs, int highpassHz, int lowpa
void BitstreamGenerator::encode(const std::string &inputPath, const std::string &inputFilename,
const std::string &outputPath) {
// Perform LPC analysis and convert audio data to a bitstream
auto bitstream = generateBitstream(inputPath);
bitstream = formatBitstream(bitstream, inputFilename);
auto frames = generateFrames(inputPath);
auto bitstream = formatBitstream(frames, inputFilename);

// Write bitstream to disk
std::ofstream lpcOut;
Expand All @@ -51,13 +51,13 @@ void BitstreamGenerator::encodeBatch(const std::vector<std::string> &inputPaths,
const std::vector<std::string> &inputFilenames, const std::string &outputPath) {
if (style == ENCODERSTYLE_ASCII) {
// Create directory to populate with encoded files
fs::create_directory(outputPath);
std::experimental::filesystem::create_directory(outputPath);

for (int i = 0; i < inputPaths.size(); i++) {
const auto& inPath = inputPaths[i];
const auto& filename = inputFilenames[i];

fs::path outPath = outputPath;
std::experimental::filesystem::path outPath = outputPath;
outPath /= (filename + ".lpc");

encode(inPath, filename, outPath.string());
Expand All @@ -70,8 +70,8 @@ void BitstreamGenerator::encodeBatch(const std::vector<std::string> &inputPaths,
const auto& inPath = inputPaths[i];
const auto& filename = inputFilenames[i];

auto bitstream = generateBitstream(inPath);
bitstream = formatBitstream(bitstream, filename);
auto frames = generateFrames(inPath);
auto bitstream = formatBitstream(frames, filename);

lpcOut << bitstream << std::endl;
}
Expand All @@ -81,7 +81,7 @@ void BitstreamGenerator::encodeBatch(const std::vector<std::string> &inputPaths,
}

// Generate a bitstream from a single audio file
std::string BitstreamGenerator::generateBitstream(const std::string &inputPath) const {
std::vector<Frame> BitstreamGenerator::generateFrames(const std::string &inputPath) const {
// Mix audio to 8kHz mono and store in a segmented buffer
auto lpcBuffer = AudioBuffer(inputPath, 8000, windowMs);

Expand Down Expand Up @@ -149,32 +149,36 @@ std::string BitstreamGenerator::generateBitstream(const std::string &inputPath)
postProcessor.detectRepeatFrames();
}

// Encode frames to hex bitstreams
auto encoder = FrameEncoder(frames, style != ENCODERSTYLE_ASCII, ',');
auto bitstream = encoder.toHex(includeStopFrame);

return bitstream;
return frames;
}

// Format raw bitstream for use in various contexts such as C headers
std::string BitstreamGenerator::formatBitstream(std::string bitstream, const std::string &filename) {
std::string BitstreamGenerator::formatBitstream(const std::vector<Frame>& frames, const std::string &filename) {
// Encode frames to hex bitstreams
auto encoder = FrameEncoder(frames, style != ENCODERSTYLE_ASCII, ',');
std::string bitstream;

// Either export the bitstream as a string for testing or as a C array for embedded development
switch(style) {
case ENCODERSTYLE_ASCII:
// ASCII-style bitstreams are the default format, and require no post-processing
bitstream = encoder.toHex(includeStopFrame);
break;
case ENCODERSTYLE_C:
// C-style bitstreams are headers which contain an integer array of bitstream values
// Format: const int bitstream_name [] = {<values>};
bitstream = "const int " + filename + "[] = {" + bitstream + "};\n";
bitstream = encoder.toHex(includeStopFrame);
bitstream = "const int " + filename + "[] = {" + bitstream + "};\n";
break;
case ENCODERSTYLE_ARDUINO:
// Arduino-style bitstreams are C-style bitstreams which include the Arduino header and PROGMEM keyword
// Format: extern const uint8_t bitstream_name [] PROGMEM = {<values>};
bitstream = "extern const uint8_t " + filename + "[] PROGMEM = {" + bitstream + "};\n";
bitstream = encoder.toHex(includeStopFrame);
bitstream = "extern const uint8_t " + filename + "[] PROGMEM = {" + bitstream + "};\n";
break;
case ENCODERSTYLE_JSON:
bitstream = encoder.toJSON();
break;
}

return bitstream;
}

2 changes: 1 addition & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ int main(int argc, char **argv) {
appEncode->add_option("-b,--highpass", highpassCutoff, "Highpass filter cutoff (Hz)");
appEncode->add_option("-l,--lowpass", lowpassCutoff, "Lowpass filter cutoff (Hz)");
appEncode->add_option("-a,--alpha", preEmphasisAlpha, "Pre-emphasis filter coefficient");
appEncode->add_option("-f,--format", bitstreamFormat, "Bitstream format: ascii (0), c (1), arduino (2)")->check(CLI::Range(0, 2));
appEncode->add_option("-f,--format", bitstreamFormat, "Bitstream format: ascii (0), c (1), arduino (2), JSON (3)")->check(CLI::Range(0, 3));
appEncode->add_flag("-n,--no-stop-frame", noStopFrame, "Do not end bitstream with stop frame");
appEncode->add_option("-g,--gain-shift", gainShift, "Quantized gain shift");
appEncode->add_option("-v,--max-voiced-gain", maxVoicedGain, "Max voiced/vowel gain (dB)");
Expand Down
3 changes: 3 additions & 0 deletions test/TmsTest.cmake
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
cmake_minimum_required(VERSION 3.14)
set(CMAKE_CXX_STANDARD 14)

include(cmake/FindJSON.cmake)

include(FetchContent)
FetchContent_Declare(googletest
GIT_REPOSITORY "https://github.com/google/googletest.git"
Expand All @@ -24,6 +26,7 @@ add_executable(TMS-Test

target_link_libraries(
TMS-Test
nlohmann_json::nlohmann_json
gtest_main
)

Expand Down

0 comments on commit cb12f84

Please sign in to comment.