FLAC support:

- FLAC container input and output support
- FLAC container analyser
- FLAC codec support in EBML input and output
This commit is contained in:
Ramkoemar 2022-10-06 15:08:28 +02:00 committed by Thulinma
parent 0f692233e8
commit a7183aedc5
17 changed files with 948 additions and 1 deletions

View file

@ -0,0 +1,183 @@
/// \file flac_analyser.cpp
/// Contains the code for the FLAC Analysing tool.
#include <iostream>
#include <string>
#include <vector>
#include "analyser_flac.h"
#include <mist/bitfields.h>
#include <mist/checksum.h>
#include <mist/flac.h>
AnalyserFLAC::AnalyserFLAC(Util::Config &conf) : Analyser(conf){
a = conf.getInteger("filter");
headerParsed = false;
curPos = 0;
bufferSize = 0;
ptr = (char *)flacBuffer.c_str();
prev_header_size = 0;
pos = NULL;
forceFill = false;
}
bool AnalyserFLAC::readMagicPacket(){
char magic[4];
if (fread(magic, 4, 1, stdin) != 1){
std::cout << "Could not read magic word - aborting!" << std::endl;
return false;
}
if (FLAC::is_header(magic)){
std::cout << "Found magic packet" << std::endl;
curPos = 4;
return true;
}
std::cout << "Not a FLAC file - aborting!" << std::endl;
return false;
}
void AnalyserFLAC::init(Util::Config &conf){
Analyser::init(conf);
JSON::Value opt;
opt["long"] = "filter";
opt["short"] = "F";
opt["arg"] = "num";
opt["default"] = "0";
opt["help"] =
"Only print information about this tag type (8 = audio, 9 = video, 18 = meta, 0 = all)";
conf.addOption("filter", opt);
opt.null();
if (feof(stdin)){
WARN_MSG("cannot open stdin");
return;
}
}
bool AnalyserFLAC::readMeta(){
if (!readMagicPacket()){return false;}
bool lastMeta = false;
char metahead[4];
while (!feof(stdin) && !lastMeta){
if (fread(metahead, 4, 1, stdin) != 1){
std::cout << "Could not read metadata block header - aborting!" << std::endl;
return 1;
}
curPos += 4;
lastMeta = (metahead[0] & 0x80); // check for last metadata block flag
std::string mType;
switch (metahead[0] & 0x7F){
case 0: mType = "STREAMINFO"; break;
case 1: mType = "PADDING"; break;
case 2: mType = "APPLICATION"; break;
case 3: mType = "SEEKTABLE"; break;
case 4: mType = "VORBIS_COMMENT"; break;
case 5: mType = "CUESHEET"; break;
case 6: mType = "PICTURE"; break;
case 127: mType = "INVALID"; break;
default: mType = "UNKNOWN"; break;
}
unsigned int bytes = Bit::btoh24(metahead + 1);
curPos += bytes;
fseek(stdin, bytes, SEEK_CUR);
std::cout << "Found metadata block of type " << mType << ", skipping " << bytes << " bytes" << std::endl;
if (mType == "STREAMINFO"){FAIL_MSG("streaminfo");}
}
INFO_MSG("last metadata");
headerParsed = true;
return true;
}
bool AnalyserFLAC::parsePacket(){
if (feof(stdin) && flacBuffer.size() < 100){
stop();
return false;
}
if (!headerParsed){
if (!readMeta()){
stop();
return false;
}
}
uint64_t needed = 40000;
// fill up buffer as we go
if (flacBuffer.length() < 8100 || forceFill){
forceFill = false;
for (int i = 0; i < needed; i++){
char tmp = std::cin.get();
bufferSize++;
flacBuffer += tmp;
if (!std::cin.good()){
WARN_MSG("End, process remaining buffer data: %zu bytes", flacBuffer.size());
if (flacBuffer.size() < 1){
FAIL_MSG("eof");
return false;
}
break;
}
}
start = &flacBuffer[0];
end = &flacBuffer[flacBuffer.size()];
}
if (!pos){pos = start + 1;}
while (pos < end - 5){
uint16_t tmp = (*pos << 8) | *(pos + 1);
if (tmp == 0xfff8){// check sync code
char *a = pos;
char u = *(a + 4);
int utfv = FLAC::utfVal(start + 4); // read framenumber
if (utfv + 1 != FLAC::utfVal(a + 4)){
FAIL_MSG("framenr: %d, end framenr: %d, size: %zu curPos: %" PRIu64, FLAC::utfVal(start + 4),
FLAC::utfVal(a + 4), (size_t)(pos - start), curPos);
}else{
INFO_MSG("framenr: %d, end framenr: %d, size: %zu curPos: %" PRIu64, FLAC::utfVal(start + 4),
FLAC::utfVal(a + 4), (size_t)(pos - start), curPos);
}
int skip = FLAC::utfBytes(u);
prev_header_size = skip + 4;
if (checksum::crc8(0, pos, prev_header_size) == *(a + prev_header_size)){
// checksum pass, valid startframe found
if (((utfv + 1 != FLAC::utfVal(a + 4))) && (utfv != 0)){
WARN_MSG("error frame, found: %d, expected: %d, ignore.. ", FLAC::utfVal(a + 4), utfv + 1);
}else{
FLAC::Frame f(start);
curPos += (pos - start);
flacBuffer.erase(0, pos - start);
start = &flacBuffer[0];
end = &flacBuffer[flacBuffer.size()];
pos = start + 2;
return true;
}
}else{
WARN_MSG("Checksum mismatch! %x - %x, curPos: %" PRIu64, *(a + prev_header_size),
checksum::crc8(0, pos, prev_header_size), curPos + (pos - start));
}
}
pos++;
}
if (std::cin.good()){
forceFill = true;
return true;
}
return false;
}

View file

@ -0,0 +1,34 @@
#pragma once
#include "analyser.h"
class AnalyserFLAC : public Analyser{
public:
AnalyserFLAC(Util::Config &conf);
bool parsePacket();
static void init(Util::Config &conf);
bool readMagicPacket();
bool readFrame();
bool readMeta();
private:
int64_t a;
uint64_t neededBytes();
void newFrame(char *data);
bool headerParsed;
std::string flacBuffer;
uint64_t bufferSize;
uint64_t curPos;
size_t utfBytes(char p);
bool forceFill;
char *ptr;
bool stopProcessing;
char *start; // = &flacBuffer[0];
char *end; // = &flacBuffer[flacBuffer.size()];
char *pos; // = start;
int prev_header_size;
FILE *inFile;
};

View file

@ -11,6 +11,7 @@ analysers = [
{'name': 'HLS', 'format': 'hls'},
{'name': 'RIFF', 'format': 'riff'},
{'name': 'RTSP', 'format': 'rtsp'},
{'name': 'FLAC', 'format': 'flac'},
]
foreach analyser : analysers

View file

@ -37,6 +37,7 @@ namespace Mist{
capa["codecs"]["audio"].append("AC3");
capa["codecs"]["audio"].append("FLOAT");
capa["codecs"]["audio"].append("DTS");
capa["codecs"]["audio"].append("FLAC");
capa["codecs"]["metadata"].append("JSON");
capa["codecs"]["subtitle"].append("subtitle");
lastClusterBPos = 0;
@ -293,6 +294,12 @@ namespace Mist{
trueCodec = "AC3";
trueType = "audio";
}
if (codec == "A_FLAC"){
trueCodec = "FLAC";
trueType = "audio";
tmpElem = E.findChild(EBML::EID_CODECPRIVATE);
if (tmpElem){init = tmpElem.getValStringUntrimmed();}
}
if (codec == "A_MPEG/L3"){
trueCodec = "MP3";
trueType = "audio";

347
src/input/input_flac.cpp Normal file
View file

@ -0,0 +1,347 @@
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <mist/defines.h>
#include <mist/stream.h>
#include <mist/util.h>
#include <string>
#include <sys/stat.h> //for stat
#include <sys/types.h> //for stat
#include <unistd.h> //for stat
#include "input_flac.h"
#include <mist/flac.h>
namespace Mist{
inputFLAC::inputFLAC(Util::Config *cfg) : Input(cfg){
capa["name"] = "FLAC";
capa["desc"] = "Allows loading FLAC files for Audio on Demand.";
capa["source_match"] = "/*.flac";
capa["source_file"] = "$source";
capa["priority"] = 9;
capa["codecs"]["audio"].append("FLAC");
stopProcessing = false;
stopFilling = false;
pos = 2;
curPos = 0;
sampleNr = 0;
frameNr = 0;
sampleRate = 0;
blockSize = 0;
bitRate = 0;
channels = 0;
tNum = INVALID_TRACK_ID;
}
inputFLAC::~inputFLAC(){}
bool inputFLAC::checkArguments(){
if (config->getString("input") == "-"){
std::cerr << "Input from stdin not yet supported" << std::endl;
return false;
}
if (!config->getString("streamname").size()){
if (config->getString("output") == "-"){
std::cerr << "Output to stdout not yet supported" << std::endl;
return false;
}
}else{
if (config->getString("output") != "-"){
std::cerr << "File output in player mode not supported" << std::endl;
return false;
}
}
return true;
}
bool inputFLAC::preRun(){
inFile = fopen(config->getString("input").c_str(), "r");
if (!inFile){return false;}
return true;
}
void inputFLAC::stripID3tag(){
char header[10];
fread(header, 10, 1, inFile); // Read a 10 byte header
if (header[0] == 'I' || header[1] == 'D' || header[2] == '3'){
uint64_t id3size = (((int)header[6] & 0x7F) << 21) | (((int)header[7] & 0x7F) << 14) |
(((int)header[8] & 0x7F) << 7) |
((header[9] & 0x7F) + 10 + ((header[5] & 0x10) ? 10 : 0));
INFO_MSG("strip ID3 Tag, size: %" PRIu64 " bytes", id3size);
curPos += id3size;
fseek(inFile, id3size, SEEK_SET);
}else{
fseek(inFile, 0, SEEK_SET);
}
}
bool inputFLAC::readMagicPacket(){
char magic[4];
if (fread(magic, 4, 1, inFile) != 1){
FAIL_MSG("Could not read magic word - aborting!");
return false;
}
if (FLAC::is_header(magic)){
curPos += 4;
return true;
}
FAIL_MSG("Not a FLAC file - aborting!");
return false;
}
bool inputFLAC::readHeader(){
if (!inFile){return false;}
if (readExistingHeader()){
WARN_MSG("header exists, read old one");
if (M.inputLocalVars.isMember("blockSize")){
return true;
}else{
INFO_MSG("Header needs update as it contains no blockSize, regenerating");
}
}
meta.reInit(config->getString("streamname"));
Util::ResizeablePointer tmpInit;
tmpInit.append("fLaC", 4);
stripID3tag();
if (!readMagicPacket()){return false;}
bool lastMeta = false;
char metahead[4];
while (!feof(inFile) && !lastMeta){
if (fread(metahead, 4, 1, inFile) != 1){
FAIL_MSG("Could not read metadata block header - aborting!");
return false;
}
uint32_t bytes = Bit::btoh24(metahead + 1);
lastMeta = (metahead[0] & 0x80); // check for last metadata block flag
std::string mType;
switch (metahead[0] & 0x7F){
case 0:{
mType = "STREAMINFO";
char metaTmp[bytes];
if (fread(metaTmp, bytes, 1, inFile) != 1){
FAIL_MSG("Could not read streaminfo metadata - aborting!");
return false;
}
// Store this block in init data
metahead[0] |= 0x80; //Set last metadata block flag
tmpInit.append(metahead, 4);
tmpInit.append(metaTmp, bytes);
HIGH_MSG("min blocksize: %zu, max: %zu", (size_t)(metaTmp[0] << 8 | metaTmp[1]),
(size_t)(metaTmp[2] << 8) | metaTmp[3]);
blockSize = (metaTmp[0] << 8 | metaTmp[1]);
if ((metaTmp[2] << 8 | metaTmp[3]) != blockSize){
FAIL_MSG("variable block size not supported!");
return 1;
}
sampleRate = ((metaTmp[10] << 12) | (metaTmp[11] << 4) | ((metaTmp[12] & 0xf0) >> 4));
WARN_MSG("setting samplerate: %zu", sampleRate);
channels = ((metaTmp[12] & 0xe) >> 1) + 1;
HIGH_MSG("setting channels: %zu", channels);
break;
}
case 1: mType = "PADDING"; break;
case 2: mType = "APPLICATION"; break;
case 3: mType = "SEEKTABLE"; break;
case 4: mType = "VORBIS_COMMENT"; break;
case 5: mType = "CUESHEET"; break;
case 6: mType = "PICTURE"; break;
case 127: mType = "INVALID"; break;
default: mType = "UNKNOWN"; break;
}
curPos += 4 + bytes;
if (mType != "STREAMINFO"){fseek(inFile, bytes, SEEK_CUR);}
INFO_MSG("Found %" PRIu32 "b metadata block of type %s", bytes, mType.c_str());
}
if (!sampleRate){
FAIL_MSG("Could not get sample rate from file header");
return false;
}
if (!channels){
FAIL_MSG("no channel information found!");
return false;
}
tNum = meta.addTrack();
meta.setID(tNum, tNum);
meta.setType(tNum, "audio");
meta.setCodec(tNum, "FLAC");
meta.setRate(tNum, sampleRate);
meta.setChannels(tNum, channels);
meta.setInit(tNum, tmpInit, tmpInit.size());
meta.inputLocalVars["blockSize"] = blockSize;
getNext();
while (thisPacket){
meta.update(thisPacket);
getNext();
}
return true;
}
bool inputFLAC::fillBuffer(size_t size){
if (feof(inFile)){
INFO_MSG("EOF");
return flacBuffer.size();
}
for (int i = 0; i < size; i++){
char tmp = fgetc(inFile);
if (!feof(inFile)){
flacBuffer += tmp;
}else{
WARN_MSG("End, process remaining buffer data: %zu bytes", flacBuffer.size());
stopFilling = true;
break;
}
}
start = (char *)flacBuffer.data();
end = start + flacBuffer.size();
return flacBuffer.size();
}
void inputFLAC::getNext(size_t idx){
while (!stopProcessing){
blockSize = M.inputLocalVars["blockSize"].asInt();
// fill buffer if needed
if (pos + 16 >= flacBuffer.size()){
if (!stopFilling){
if (!fillBuffer(1000)){
thisPacket.null();
return;
}
}else{
if (!flacBuffer.size()){
thisPacket.null();
return;
}
}
}
if (stopFilling && ((flacBuffer.size() - pos) < 1)){
uint64_t timestamp = (sampleNr * 1000) / sampleRate;
HIGH_MSG("process last frame size: %zu", flacBuffer.size());
thisTime = timestamp;
thisIdx = tNum;
thisPacket.genericFill(timestamp, 0, 0, start, flacBuffer.size(), curPos, false);
flacBuffer.clear();
stopProcessing = true;
return;
}
uint16_t tmp = Bit::btohs(start + pos);
if (tmp == 0xfff8){// check sync code
char *a = start + pos;
int utfv = FLAC::utfVal(start + 4); // read framenumber
int skip = FLAC::utfBytes(*(a + 4));
prev_header_size = skip + 4;
if (checksum::crc8(0, start + pos, prev_header_size) == *(a + prev_header_size)){
// checksum pass, valid frame found
if (((utfv + 1 != FLAC::utfVal(a + 4))) &&
(utfv != 0)){// TODO: this check works only if framenumbers are used in the header
HIGH_MSG("error frame, found: %d, expected: %d, ignore.. ", FLAC::utfVal(a + 4), utfv + 1);
// checksum pass, but incorrect framenr... happens sometimes
}else{
// FLAC_Frame f(start);
FLAC::Frame f(start);
if (sampleRate != 0 && f.rate() != sampleRate){
FAIL_MSG("samplerate from frame header: %d, sampleRate from file header: %zu", f.rate(), sampleRate);
thisPacket.null();
return;
}
frameNr++;
uint64_t timestamp = (sampleNr * 1000) / sampleRate;
if (blockSize != f.samples()){
FAIL_MSG("blockSize differs, %zu %u", blockSize, f.samples());
}
if (blockSize == 0){
FAIL_MSG("Cannot detect block size");
return;
}
if (blockSize == 1 || blockSize == 2){
// get blockSize from end of header
// TODO: need to test
FAIL_MSG("weird blocksize");
blockSize = *(start + prev_header_size); // 8 or 16 bits value
}
sampleNr += blockSize;
thisTime = timestamp;
thisIdx = tNum;
thisPacket.genericFill(timestamp, 0, 0, start, pos, curPos, false);
curPos += pos;
flacBuffer.erase(0, pos);
start = (char *)flacBuffer.data();
end = start + flacBuffer.size();
pos = 2;
return;
}
}
}
pos++;
}
thisPacket.null();
return;
}
void inputFLAC::seek(uint64_t seekTime, size_t idx){
uint64_t mainTrack = M.mainTrack();
blockSize = M.inputLocalVars["blockSize"].asInt();
sampleRate = meta.getRate(mainTrack);
pos = 2;
clearerr(inFile);
flacBuffer.clear();
stopProcessing = false;
stopFilling = false;
DTSC::Keys keys(M.keys(mainTrack));
DTSC::Parts parts(M.parts(mainTrack));
uint64_t seekPos = keys.getBpos(0);
uint64_t seekKeyTime = keys.getTime(0);
// Replay the parts of the previous keyframe, so the timestaps match up
for (size_t i = 0; i < keys.getEndValid(); i++){
if (keys.getTime(i) > seekTime){break;}
DONTEVEN_MSG("Seeking to %" PRIu64 ", found %" PRIu64 "...", seekTime, keys.getTime(i));
seekPos = keys.getBpos(i);
seekKeyTime = keys.getTime(i);
}
Util::fseek(inFile, seekPos, SEEK_SET);
sampleNr = (uint64_t)((seekKeyTime * sampleRate / 1000 + (blockSize / 2)) / blockSize) * blockSize;
HIGH_MSG("seek: %" PRIu64 ", sampleNr: %" PRIu64 ", time: %" PRIu64, seekPos, sampleNr, seekKeyTime);
curPos = seekPos;
return;
}
}// namespace Mist

48
src/input/input_flac.h Normal file
View file

@ -0,0 +1,48 @@
#include "input.h"
#include <mist/checksum.h>
#include <mist/dtsc.h>
namespace Mist{
class inputFLAC : public Input{
public:
inputFLAC(Util::Config *cfg);
~inputFLAC();
protected:
bool checkArguments();
bool preRun();
bool readHeader();
void getNext(size_t idx = INVALID_TRACK_ID);
void seek(uint64_t seekTime, size_t idx = INVALID_TRACK_ID);
FILE *inFile;
private:
void stripID3tag();
bool readMagicPacket();
bool fillBuffer(size_t size = 40960);
uint64_t neededBytes();
char *ptr;
std::string flacBuffer;
Socket::Buffer buffer;
uint64_t bufferSize;
uint64_t curPos;
bool stopProcessing;
bool stopFilling;
size_t tNum;
size_t pos;
char *end;
char *start;
int prev_header_size;
uint64_t sampleNr;
uint64_t frameNr;
size_t sampleRate;
size_t blockSize;
size_t bitRate;
size_t channels;
};
}// namespace Mist
typedef Mist::inputFLAC mistIn;

View file

@ -17,6 +17,7 @@ inputs = [
{'name' : 'SRT', 'format' : 'srt'},
{'name' : 'SDP', 'format' : 'sdp'},
{'name' : 'AAC', 'format' : 'aac'},
{'name' : 'FLAC', 'format' : 'flac'},
]
#Referenced by process targets

View file

@ -6,6 +6,7 @@ outputs = [
{'name' : 'HTTPMinimalServer', 'format' : 'http_minimalserver', 'extra': ['http']},
{'name' : 'MP4', 'format' : 'mp4', 'extra': ['http']},
{'name' : 'AAC', 'format' : 'aac', 'extra': ['http']},
{'name' : 'FLAC', 'format' : 'flac', 'extra': ['http']},
{'name' : 'MP3', 'format' : 'mp3', 'extra': ['http']},
{'name' : 'H264', 'format' : 'h264', 'extra': ['http']},
{'name' : 'HDS', 'format' : 'hds', 'extra': ['http']},

View file

@ -67,6 +67,7 @@ namespace Mist{
capa["codecs"][0u][0u].append("MPEG2");
capa["codecs"][0u][0u].append("AV1");
capa["codecs"][0u][1u].append("AAC");
capa["codecs"][0u][1u].append("FLAC");
capa["codecs"][0u][1u].append("vorbis");
capa["codecs"][0u][1u].append("opus");
capa["codecs"][0u][1u].append("PCM");
@ -194,6 +195,7 @@ namespace Mist{
if (codec == "PCM"){return "A_PCM/INT/BIG";}
if (codec == "MP2"){return "A_MPEG/L2";}
if (codec == "MP3"){return "A_MPEG/L3";}
if (codec == "FLAC"){return "A_FLAC";}
if (codec == "AC3"){return "A_AC3";}
if (codec == "ALAW"){return "A_MS/ACM";}
if (codec == "ULAW"){return "A_MS/ACM";}

View file

@ -0,0 +1,50 @@
#include "output_flac.h"
namespace Mist{
OutFLAC::OutFLAC(Socket::Connection &conn) : HTTPOutput(conn){}
void OutFLAC::init(Util::Config *cfg){
HTTPOutput::init(cfg);
capa["name"] = "FLAC";
capa["friendly"] = "Free Lossless Audio Codec";
capa["desc"] = "Pseudostreaming in FLAC format over HTTP";
capa["url_rel"] = "/$.flac";
capa["url_match"] = "/$.flac";
capa["codecs"][0u][0u].append("FLAC");
capa["methods"][0u]["handler"] = "http";
capa["methods"][0u]["type"] = "html5/audio/flac";
capa["methods"][0u]["hrn"] = "FLAC progressive";
capa["methods"][0u]["priority"] = 8;
JSON::Value opt;
opt["arg"] = "string";
opt["default"] = "";
opt["arg_num"] = 1;
opt["help"] = "Target filename to store FLAC file as, or - for stdout.";
cfg->addOption("target", opt);
}
void OutFLAC::sendNext(){
char *dataPointer = 0;
size_t len = 0;
thisPacket.getString("data", dataPointer, len);
myConn.SendNow(dataPointer, len);
}
void OutFLAC::sendHeader(){
myConn.SendNow(M.getInit(M.mainTrack()));
sentHeader = true;
}
void OutFLAC::respondHTTP(const HTTP::Parser &req, bool headersOnly){
// Set global defaults
HTTPOutput::respondHTTP(req, headersOnly);
H.StartResponse("200", "OK", req, myConn);
if (headersOnly){return;}
parseData = true;
wantRequest = false;
}
}// namespace Mist

17
src/output/output_flac.h Normal file
View file

@ -0,0 +1,17 @@
#include "output_http.h"
namespace Mist{
class OutFLAC : public HTTPOutput{
public:
OutFLAC(Socket::Connection &conn);
static void init(Util::Config *cfg);
virtual void respondHTTP(const HTTP::Parser &req, bool headersOnly);
void sendNext();
void sendHeader();
private:
bool isFileTarget(){return isRecording();}
};
}// namespace Mist
typedef Mist::OutFLAC mistOut;