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 →
What we have covered in the first-part:
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.