C++ and Node.js Integration: Part 2

As promised, I’m here with the second part of this blog series 😎. In the first-part we discussed about the benefit of integrating c++ in our node.js project, along with the required tools & step by step guide to setup the c++ in an existing or new project. If you haven’t read the first-part yet my suggestion is to read it first then come back. Because here I’m going to assume that you have some knowledge of c++ and node.js along with it’s integration part. You can read the first-part here →

Breaking Boundaries: Discover the Magic of C++ and Node.js Integration (Part 1 / 2).

In this article, we delve into the possibilities of combining c++ & node.js, pushing the boundaries of what your…

blog.devgenius.io

What we have covered in the first-part:

  • Benefit of utilizing c++ in node.js.
  • Areas where we should / might consider integrating both the technologies.
  • Installing the required tools and setting up the c++ build environment.
  • Writing our first Hello World program in c++ and calling it via node.js as a normal JavaScript function.

What are we going to cover here ?

As promised in the first-part itself that we’ll write the bindings for a third party c++ library & use it in our project. We will create a program which will extract the meta-data of an audio file ex- artist, genre, album, comment, bit-rate, duration, etc. And our program can also extract the cover image of audio file. For that we’re going to use a c++ library called taglib.

TagLib is a library for reading and editing the meta-data of several popular audio formats.

We’ll start with creating a new node.js project & then adding the taglib library into it. Create it running npm init -y or yarn init inside an empty folder. And setup the integration scripts & files (refer to first-part).

It should look like this-

Now we’ll download the taglib’s source code from here -> https://taglib.org/releases/taglib-1.13.tar.gz extract it & add it to our project. Create a file build-taglib.sh and paste the below content to it.

cd taglib-1.13

cmake \
    -DCMAKE_INSTALL_PREFIX="`pwd`/_install" \
    -DCMAKE_BUILD_TYPE=Release \
    -DBUILD_SHARED_LIBS=OFF .

make

make install

This script is responsible for compiling taglib & creating the static lib (.a) which we’ll use. Now add build:taglib: chmod +x build-taglib.sh && ./build-taglib.sh in script of package.json.

Just run npm run build:taglib it will take couple of seconds to build, if everything goes well a new folder _install will be created inside taglib-1.13 & that’s where we’ll find the headers & static lib.

Now we need to add taglib’s header & static lib file (_install/lib/libtag.a) in binding.gyp file, so that it can be linked during build time. Updated binding.gyp file will look like this-

{
    "variables": {
        "taglib_dir": "<!@(pwd)/taglib-1.13/_install"
    },
    "targets": [
        {
            "target_name": "node_cpp",
            "cflags!": ["-fno-exceptions"],
            "cflags_cc!": ["-fno-exceptions"],
            "include_dirs": [
                "<!@(node -p \"require('node-addon-api').include\")",
                "<(taglib_dir)/include"
            ],
            'defines': ['NAPI_DISABLE_CPP_EXCEPTIONS'],
            "libraries": [
                "<(taglib_dir)/lib/libtag.a"
            ]
        }
    ]
}

We have just completed the 65-70% of our major work, congratulations to us 🥳 🥳. Now we’ll write the bindings in c++ and use it in JavaScript code.

Create a folder named src inside the project and create 5 files into it- binding.cpp audio_metadata.cpp / audio_metadata.h audio_cover_image.cpp / audio_cover_image.h and add the .cpp files to the binding.gyp.

{
    --------
    --------
    "targets": [
        {
            ---------------
            ---------------
            "sources": [
                "./src/binding.cpp",
                "./src/audio_cover_image.cpp",
                "./src/audio_metadata.cpp"
            ],
            ---------------
            ---------------
        }
    ]
}

I’ll give the code for each file, I’m assuming you already have some knowledge of c++.

audio_metadata.h

#ifndef _AUDIO_METADATA_
#define _AUDIO_METADATA_

#include <string>
#include <napi.h>
#include <map>
#include <taglib/fileref.h>

std::map<std::string, std::string> __get_exif_data(std::string file_path);

Napi::Value getMetaData(const Napi::CallbackInfo &info);

#endif

audio_metadata.cpp

#include "audio_metadata.h"

std::map<std::string, std::string> __get_exif_data(std::string file_path)
{
    std::map<std::string, std::string> map;
    TagLib::FileRef f = TagLib::FileRef(file_path.c_str());
    auto audio_props = f.audioProperties();
    auto tag = f.tag();
    auto album = tag->album();
    auto artist = tag->artist();
    auto comment = tag->comment();
    auto genre = tag->genre();
    auto title = tag->title();
    map["album"] = album.isNull() ? "" : album.to8Bit();
    map["artist"] = artist.isNull() ? "" : artist.to8Bit();
    map["comment"] = comment.isNull() ? "" : comment.to8Bit();
    map["genre"] = genre.isNull() ? "" : genre.to8Bit();
    map["title"] = title.isNull() ? "" : title.to8Bit();
    map["track"] = std::to_string(tag->track());
    map["year"] = std::to_string(tag->year());
    map["bitrate"] = audio_props ? std::to_string(audio_props->bitrate()) : "";
    map["channels"] = audio_props ? std::to_string(audio_props->channels()) : "";
    map["sampleRate"] = audio_props ? std::to_string(audio_props->sampleRate()) : "";
    map["duration"] = audio_props ? std::to_string(audio_props->length()) : "";
    return map;
}

Napi::Value getMetaData(const Napi::CallbackInfo &info)
{
    Napi::Env env = info.Env();
    if (info.Length() <= 0)
    {
        Napi::Error::New(env, "getMetaData: expects one argument as filepath.").ThrowAsJavaScriptException();
        return env.Null();
    }
    if (!info[0].IsString())
    {
        Napi::Error::New(env, "getMetaData: expects argument of type string.").ThrowAsJavaScriptException();
        return env.Null();
    }
    std::string file_path = info[0].ToString();
    auto map = __get_exif_data(file_path);
    Napi::Object ret_obj = Napi::Object::New(env);
    for (auto i = map.begin(); i != map.end(); i++)
    {
        ret_obj.Set(i->first, Napi::String::New(env, i->second));
    }
    return ret_obj;
}

This function expects an audio file path, it extracts all the available meta-data & sends back to JS side.

audio_cover_image.h

#ifndef _AUDIO_COVER_IMAGE_
#define _AUDIO_COVER_IMAGE_

#include <iostream>
#include <sys/stat.h>
#include <cstdlib>
#include <napi.h>
#include <taglib/tag.h>
#include <taglib/fileref.h>
#include <taglib/tpropertymap.h>
#include <taglib/id3v1tag.h>
#include <taglib/id3v2tag.h>
#include <taglib/mpegfile.h>
#include <taglib/id3v2frame.h>
#include <taglib/id3v2header.h>
#include <taglib/attachedpictureframe.h>

inline static const char *PICTURE_ID = "APIC";

struct Cover_Info
{
    unsigned long size = 0;
    // can be "image/png" or "image/jpeg".
    std::string mime_type = "";
    char *cover_art_data = nullptr;

    ~Cover_Info()
    {
        if (cover_art_data != nullptr)
            delete cover_art_data;
    }
};

void __get_cover_data_byte(std::string file_path, Cover_Info *cover_info);

void getCoverImage(const Napi::CallbackInfo &info);

#endif

audio_cover_image.cpp

#include "audio_cover_image.h";

void __get_cover_data_byte(std::string file_path, Cover_Info *cover_info)
{
    TagLib::MPEG::File mpeg_file(file_path.c_str());
    TagLib::ID3v2::Tag *id3v2_tag = mpeg_file.ID3v2Tag();
    if (id3v2_tag)
    {
        TagLib::ID3v2::FrameList frame_list;
        TagLib::ID3v2::AttachedPictureFrame *pic_frame;
        frame_list = id3v2_tag->frameListMap()[PICTURE_ID];
        if (!frame_list.isEmpty())
        {
            for (auto it = frame_list.begin(); it != frame_list.end(); ++it)
            {
                pic_frame = (TagLib::ID3v2::AttachedPictureFrame *)(*it);
                if (pic_frame->type() == TagLib::ID3v2::AttachedPictureFrame::FrontCover)
                {
                    if (pic_frame->picture().size() > 0)
                    {
                        cover_info->mime_type = pic_frame->mimeType().to8Bit();
                        cover_info->size = pic_frame->picture().size();
                        cover_info->cover_art_data = new char[cover_info->size]();
                        memcpy(cover_info->cover_art_data, pic_frame->picture().data(), cover_info->size);
                    }
                }
            }
        }
    }
}

void getCoverImage(const Napi::CallbackInfo &info)
{
    Napi::Env env = info.Env();
    const Napi::Function callback = info[1].As<Napi::Function>();
    if (info.Length() < 2)
    {
        Napi::Error::New(env, "getCoverImage: expects 2 argument.").ThrowAsJavaScriptException();
        callback.Call({env.Null()});
        return;
    }
    if (!info[0].IsString())
    {
        Napi::Error::New(env, "getCoverImage: expects 1st argument of type string.").ThrowAsJavaScriptException();
        callback.Call({env.Null()});
        return;
    }
    if (!info[1].IsFunction())
    {
        Napi::Error::New(env, "getCoverImage: expects 2nd argument of type function.").ThrowAsJavaScriptException();
        callback.Call({env.Null()});
        return;
    }
    const std::string file_path = info[0].ToString();
    Cover_Info cover_info;
    __get_cover_data_byte(file_path, &cover_info);
    uint8_t *buffer = (uint8_t *)cover_info.cover_art_data;
    const auto uint8_buffer = Napi::Buffer<uint8_t>::New(env, buffer, cover_info.size);
    callback.Call({uint8_buffer});
}

This is responsible for extracting the cover image data. This method expects 2 argument, 1st one is the audio file path & 2nd is a callback which will be invoked with image data-buffer or null in-case of any error.

And the final & most important

binding.cpp

#include "audio_cover_image.h"
#include "audio_metadata.h"

Napi::Object init(Napi::Env env, Napi::Object exports)
{
    exports.Set(
        "getMetaData",
        Napi::Function::New(env, getMetaData));
    exports.Set(
        "getCoverImage",
        Napi::Function::New(env, getCoverImage));
    return exports;
}

NODE_API_MODULE(node_taglib, init);

Here we’re exposing our methods to JS side.

Now we’ll configure & build our lib that’s the most easiest part, just run node-gyp configure and then node-gyp build. You will notice a new folder build has been created. Our lib is located inside build/Release/node_cpp.node. We can directly import this file in our node.js project & use it.

Now it’s time to test:

We have done a lot of work, now it’s time to actually validate if any of this makes any sense.

Create a folder test inside our project and create index.js file and also add your favorite MP3 file to play with.

test/index.js

const fs = require("fs");
const node_cpp = require("../build/Release/node_cpp");

const audioFile = `${process.cwd()}/test/heart-of-gold.mp3`;

// example for reading meta-data from an audio file.
const readMetaData = () => {
    const metadata = node_cpp.getMetaData(audioFile);
    console.log(metadata);
}

// example for extracting cover image from an audio file.
const extractCoverImage = () => {
    node_cpp.getCoverImage(audioFile, buffer => {
        if(buffer !== null) {
            const newFile = `${process.cwd()}/test/cover-image.jpg`;
            fs.writeFileSync(newFile, buffer);
            console.log("cover image saved...");
        }
    });
}

readMetaData();

extractCoverImage();

Now if you run this you’ll see that audio’s meta data is printed on the console along the a new file cover-image.jpg is created.

Here is the sample output-

audio meta-data info along with cover image.

We finally did it 🤩 🥳

Where we can go from here:

Now it’s up to you what you want to build. Taglib also provide APIs to update the metadata & set new cover image. Using that you can build an app to edit audio file’s meta data or change the cover image. Even you can utilize it on your back-end.


#c #cpluplus #node 

C++ and Node.js Integration: Part 2
1.00 GEEK