#include "flactomp3.h" #include constexpr uint16_t flacDefaultMaxBlockSize = 4096; constexpr uint16_t id3v1TagSize = 128; // constexpr std::wstring_view BOM ({(0xFEFF}); //one day... constexpr std::string_view title ("TITLE="); constexpr std::string_view artist ("ARTIST="); constexpr std::string_view album ("ALBUM="); constexpr std::string_view comment ("COMMENT="); constexpr std::string_view genre ("GENRE="); constexpr std::string_view track ("TRACKNUMBER="); constexpr std::string_view date ("DATE="); constexpr std::string_view jpeg ("image/jpeg"); static const std::map knownTags { {"ALBUMARTIST=", "TPE2="}, {"PUBLISHER=", "TPUB="}, {"LENGTH=", "TLEN="}, {"ISRC=", "TSRC="}, {"DISCNUMBER=", "TPOS="}, {"BPM=", "TBPM="}, {"LYRICS=", "USLT="} //but it's not supported in LAME }; FLACtoMP3::FLACtoMP3(Severity severity, uint8_t size) : Loggable(severity), inPath(), outPath(), decoder(FLAC__stream_decoder_new()), encoder(lame_init()), statusFLAC(), output(nullptr), bufferMultiplier(size), flacMaxBlockSize(0), pcmCounter(0), pcmSize(0), pcm(nullptr), outputBuffer(nullptr), outputBufferSize(0), outputInitilized(false), downscaleAlbumArt(false), usc2convertor() { } FLACtoMP3::~FLACtoMP3() { lame_close(encoder); FLAC__stream_decoder_delete(decoder); } bool FLACtoMP3::run() { FLAC__bool ok = FLAC__stream_decoder_process_until_end_of_stream(decoder); uint32_t fileSize; if (ok) { if (pcmCounter > 0) flush(); int nwrite = lame_encode_flush(encoder, outputBuffer, pcmSize * 2); fwrite((char*)outputBuffer, nwrite, 1, output); if (downscaleAlbumArt) { fileSize = ftell(output); lame_mp3_tags_fid(encoder, output); } else { int tag1Size = lame_get_id3v1_tag(encoder, outputBuffer, id3v1TagSize); if (tag1Size > id3v1TagSize) log(warning, "couldn't write id3v1 tag"); else fwrite((char*)outputBuffer, tag1Size, 1, output); fileSize = ftell(output); fseek(output, 0, SEEK_SET); int tag2Size = lame_get_id3v2_tag(encoder, outputBuffer, outputBufferSize); if (tag2Size > outputBufferSize) { log(Loggable::error, "couldn't write id3v2 tag"); } else fwrite((char*)outputBuffer, tag2Size, 1, output); int vbrTagSize = lame_get_lametag_frame(encoder, outputBuffer, outputBufferSize); if (vbrTagSize > outputBufferSize) log(Loggable::error, "couldn't write vbr tag"); fwrite((char*)outputBuffer, vbrTagSize, 1, output); } } // std::cout << " state: " << FLAC__StreamDecoderStateString[FLAC__stream_decoder_get_state(decoder)] << std::endl; if (outputInitilized) { fclose(output); output = nullptr; delete[] pcm; delete[] outputBuffer; pcm = nullptr; outputBuffer = nullptr; pcmSize = 0; flacMaxBlockSize = 0; outputBufferSize = 0; if (ok) { float MBytes = (float)fileSize / 1024 / 1024; std::string strMBytes = std::to_string(MBytes); strMBytes = strMBytes.substr(0, strMBytes.find(".") + 3) + " MiB"; log(info, "resulting file size: " + strMBytes); } return ok; } return false; } void FLACtoMP3::setInputFile(const std::string& path) { if (inPath.size() > 0) throw 1; inPath = path; FLAC__stream_decoder_set_md5_checking(decoder, true); FLAC__stream_decoder_set_metadata_respond_all(decoder); statusFLAC = FLAC__stream_decoder_init_file(decoder, path.c_str(), write, metadata, error, this); } void FLACtoMP3::setOutputFile(const std::string& path) { if (outPath.size() > 0) throw 2; outPath = path; lame_set_VBR(encoder, vbr_default); lame_set_VBR_quality(encoder, 0); lame_set_quality(encoder, 0); if (!downscaleAlbumArt) lame_set_write_id3tag_automatic(encoder, 0); } bool FLACtoMP3::initializeOutput() { if (outputInitilized) throw 5; output = fopen(outPath.c_str(), "w+b"); if (output == 0) { output = nullptr; log(fatal, "Error opening file " + outPath); return false; } int ret = lame_init_params(encoder); if (ret < 0) { log(fatal, "Error initializing LAME parameters. Code = " + std::to_string(ret)); fclose(output); output = nullptr; return false; } if (flacMaxBlockSize == 0) flacMaxBlockSize = flacDefaultMaxBlockSize; pcmSize = lame_get_num_channels(encoder) * flacMaxBlockSize * bufferMultiplier; outputBufferSize = pcmSize / 2; if (!downscaleAlbumArt) { int tag2Size = lame_get_id3v2_tag(encoder, nullptr, 0); int vbrTagSize = lame_get_lametag_frame(encoder, nullptr, 0); fseek(output, tag2Size + vbrTagSize, SEEK_SET); //reserve place for tags if (tag2Size > outputBufferSize) outputBufferSize = tag2Size; } pcm = new int16_t[pcmSize]; outputBuffer = new uint8_t[outputBufferSize]; outputInitilized = true; return true; } void FLACtoMP3::processInfo(const FLAC__StreamMetadata_StreamInfo& info) { lame_set_in_samplerate(encoder, info.sample_rate); lame_set_num_channels(encoder, info.channels); flacMaxBlockSize = info.max_blocksize; log(Loggable::info, "sample rate: " + std::to_string(info.sample_rate)); log(Loggable::info, "channels: " + std::to_string(info.channels)); log(Loggable::info, "bits per sample: " + std::to_string(info.bits_per_sample)); } void FLACtoMP3::processTags(const FLAC__StreamMetadata_VorbisComment& tags) { bool artistSet = false; //in vorbis comment there could be more then 1 artist tag for (FLAC__uint32 i = 0; i < tags.num_comments; ++i) { //usualy it's about guested and fetured artists const FLAC__StreamMetadata_VorbisComment_Entry& entry = tags.comments[i]; //gonna keep only the first one for now std::string_view comm((const char*)entry.entry); if (comm.find(title) == 0) { setTagTitle(comm.substr(title.size())); } else if (comm.find(artist) == 0) { if (!artistSet) { setTagArtist(comm.substr(artist.size())); artistSet = true; } else { log(minor, "more than one artist tag, ignoring " + std::string(comm.substr(artist.size()))); } } else if (comm.find(album) == 0) { setTagAlbum(comm.substr(album.size())); } else if (comm.find(comment) == 0) { id3tag_set_comment(encoder, comm.substr(comment.size()).data()); } else if (comm.find(genre) == 0) { id3tag_set_genre(encoder, comm.substr(genre.size()).data()); } else if (comm.find(track) == 0) { id3tag_set_track(encoder, comm.substr(track.size()).data()); } else if (comm.find(date) == 0) { std::string_view fullDate = comm.substr(date.size()); if (fullDate.size() == 10) { //1999-11-11 kind of string std::string_view month = fullDate.substr(5, std::size_t(2)); std::string_view day = fullDate.substr(8); std::string md = "TDAT=" + std::string(day) + std::string(month); int res = id3tag_set_fieldvalue(encoder, md.c_str()); if (res != 0) log(warning, "wasn't able to set the date tag (" + md + ")"); fullDate = fullDate.substr(0, 4); //year; } id3tag_set_year(encoder, std::string(fullDate).data()); } else if (!tryKnownTag(comm)) { std::string tag = "TXXX=" + std::string(comm); int res = id3tag_set_fieldvalue(encoder, tag.c_str()); if (res != 0) log(warning, "wasn't able to set user tag (" + tag + ")"); } } } void FLACtoMP3::processPicture(const FLAC__StreamMetadata_Picture& picture) { if (downscaleAlbumArt && picture.data_length > LAME_MAXALBUMART) { log(info, "embeded album art is too big (" + std::to_string(picture.data_length) + " bytes), rescaling"); log(debug, "mime type is " + std::string(picture.mime_type)); if (picture.mime_type == jpeg) { if (scaleJPEG(picture)) log(debug, "successfully rescaled album art"); else log(warning, "failed to rescale album art"); } } else { int result = id3tag_set_albumart(encoder, (const char*)picture.data, picture.data_length); if (result != 0) log(warning, "couldn't set album art tag, errcode: " + std::to_string(result)); } } bool FLACtoMP3::scaleJPEG(const FLAC__StreamMetadata_Picture& picture) { struct jpeg_decompress_struct dinfo; struct jpeg_error_mgr derr; dinfo.err = jpeg_std_error(&derr); jpeg_create_decompress(&dinfo); jpeg_mem_src(&dinfo, picture.data, picture.data_length); int rc = jpeg_read_header(&dinfo, TRUE); if (rc != 1) { log(Loggable::error, "error reading jpeg header"); return false; } uint64_t mem_size = 0; uint8_t *mem = new uint8_t[LAME_MAXALBUMART + 1024 * 4]; //I allocate a little bit more not to corrupt someone else's the memory dinfo.scale_num = 2; //need to tune it, feels like 500 by 500 is a good size dinfo.scale_denom = 3; jpeg_start_decompress(&dinfo); struct jpeg_compress_struct cinfo; struct jpeg_error_mgr cerr; cinfo.err = jpeg_std_error(&cerr); jpeg_create_compress(&cinfo); jpeg_mem_dest(&cinfo, &mem, &mem_size); cinfo.image_width = dinfo.output_width; cinfo.image_height = dinfo.output_height; cinfo.input_components = dinfo.output_components; cinfo.in_color_space = dinfo.out_color_space; jpeg_set_defaults(&cinfo); jpeg_start_compress(&cinfo, TRUE); uint32_t rowSize = dinfo.image_width * dinfo.output_components; uint8_t* row = new uint8_t[rowSize]; while (dinfo.output_scanline < dinfo.output_height) { jpeg_read_scanlines(&dinfo, &row, 1); jpeg_write_scanlines(&cinfo, &row, 1); if (mem_size > LAME_MAXALBUMART) { delete[] mem; log(Loggable::error, "resized album art exceeded reserved buffer size, stopping before memory corrution"); return false; } } jpeg_finish_decompress(&dinfo); jpeg_destroy_decompress(&dinfo); jpeg_finish_compress(&cinfo); jpeg_destroy_compress(&cinfo); delete[] row; int result = id3tag_set_albumart(encoder, (const char*)mem, mem_size); delete[] mem; return result == 0; } bool FLACtoMP3::tryKnownTag(std::string_view source) { for (const std::pair& pair : knownTags) { if (source.find(pair.first) == 0) { if (pair.first == "LYRICS=") { log(warning, "lyrics tag was not transfered, it is not supported currently"); } else if (pair.first == "BPM=") { float bpm = std::atof(source.substr(pair.first.size()).data()); bpm = std::round(bpm); //id3 bpm must be integer int result = id3tag_set_fieldvalue(encoder, std::string(pair.second + std::to_string((int)bpm)).c_str()); if (result != 0) log(warning, "wasn't able to set BPM tag. Code = " + std::to_string(result)); } else if (pair.first == "ALBUMARTIST=") { int res = setTagUSC2("TPE2", source.substr(pair.first.size())); if (res != 0) log(warning, "wasn't able to album artist tag. Code = " + std::to_string(res)); } else { std::string tag = pair.second + std::string(source.substr(pair.first.size())); int res = id3tag_set_fieldvalue(encoder, tag.c_str()); if (res != 0) log(warning, "wasn't able to set tag (" + std::string(source) + "). Code = " + std::to_string(res)); } return true; } } return false; } bool FLACtoMP3::decodeFrame(const int32_t * const buffer[], uint32_t size) { if (!outputInitilized) { bool success = initializeOutput(); if (!success) return false; } for (size_t i = 0; i < size; ++i) { pcm[pcmCounter++] = (int16_t)buffer[0][i]; pcm[pcmCounter++] = (int16_t)buffer[1][i]; if (pcmCounter == pcmSize) return flush(); } return true; } bool FLACtoMP3::flush() { int nwrite = lame_encode_buffer_interleaved( encoder, pcm, pcmCounter / 2, outputBuffer, outputBufferSize ); while (nwrite == -1) { //-1 is returned when there was not enough space in the given buffer log(major, std::to_string(outputBufferSize) + " bytes in the output buffer wasn't enough");; outputBufferSize = outputBufferSize * 2; delete[] outputBuffer; outputBuffer = new uint8_t[outputBufferSize]; log(major, "allocating " + std::to_string(outputBufferSize) + " bytes"); nwrite = lame_encode_buffer_interleaved( encoder, pcm, pcmCounter, outputBuffer, outputBufferSize ); } if (nwrite > 0) { int actuallyWritten = fwrite((char*)outputBuffer, nwrite, 1, output); pcmCounter = 0; return actuallyWritten == 1; } else { if (nwrite == 0) { log(minor, "encoding flush encoded 0 bytes, skipping write"); return true; } else { log(fatal, "encoding flush failed. Code = : " + std::to_string(nwrite)); return false; } } } void FLACtoMP3::metadata(const FLAC__StreamDecoder* decoder, const FLAC__StreamMetadata* metadata, void* client_data) { (void)(decoder); FLACtoMP3* self = static_cast(client_data); switch (metadata->type) { case FLAC__METADATA_TYPE_STREAMINFO: self->processInfo(metadata->data.stream_info); break; case FLAC__METADATA_TYPE_VORBIS_COMMENT: self->processTags(metadata->data.vorbis_comment); break; case FLAC__METADATA_TYPE_PICTURE: self->processPicture(metadata->data.picture); break; default: break; } } FLAC__StreamDecoderWriteStatus FLACtoMP3::write( const FLAC__StreamDecoder* decoder, const FLAC__Frame* frame, const FLAC__int32 * const buffer[], void* client_data ) { (void)(decoder); // if (decoded->channels != 2 || decoded->depth != 16) { // std::cout << "ERROR: this example only supports 16bit stereo streams" << std::endl; // return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; // } FLACtoMP3* self = static_cast(client_data); if (frame->header.channels != 2) { self->log(fatal, "ERROR: This frame contains " + std::to_string(frame->header.channels) + " channels (should be 2)"); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } if (buffer[0] == NULL) { self->log(fatal, "ERROR: buffer [0] is NULL"); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } if (buffer[1] == NULL) { self->log(fatal, "ERROR: buffer [1] is NULL"); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } bool result = self->decodeFrame(buffer, frame->header.blocksize); if (result) return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; else return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } void FLACtoMP3::error(const FLAC__StreamDecoder* decoder, FLAC__StreamDecoderErrorStatus status, void* client_data) { (void)decoder; FLACtoMP3* self = static_cast(client_data); std::string errText(FLAC__StreamDecoderErrorStatusString[status]); self->log(Loggable::error, "Got error callback: " + errText); } void FLACtoMP3::setTagTitle(const std::string_view& title) { id3tag_set_title(encoder, title.data()); //to set at least some possibly encoding broken id3v1 tag int res = setTagUSC2("TIT2", title); if (res != 0) log(warning, "Couldn't set Title tag. Code = " + std::to_string(res)); } void FLACtoMP3::setTagAlbum(const std::string_view& album) { id3tag_set_album(encoder, album.data()); //to set at least some possibly encoding broken id3v1 tag int res = setTagUSC2("TALB", album); if (res != 0) log(warning, "Couldn't set Album tag. Code = " + std::to_string(res)); } void FLACtoMP3::setTagArtist(const std::string_view& artist) { id3tag_set_artist(encoder, artist.data()); //to set at least some possibly encoding broken id3v1 tag int res = setTagUSC2("TPE1", artist); if (res != 0) log(warning, "Couldn't set Artist tag. Code = " + std::to_string(res)); } int FLACtoMP3::setTagUSC2(const std::string_view& field, const std::string_view& value) { std::u16string utf16 = std::u16string({0xFEFF}) + usc2convertor.from_bytes(value.data()); return id3tag_set_textinfo_utf16(encoder, field.data(), (unsigned short*)utf16.c_str()); }