text: /*! \file    pp-opus.c
   * \author   Lorenzo Miniero <lorenzo@meetecho.com>
   * \copyright GNU General Public License v3
   * \brief    Post-processing to generate .opus files
   * \details  Implementation of the post-processing code (based on libogg)
   * needed to generate .opus files out of Opus RTP frames.
   *
   * \ingroup postprocessing
   * \ref postprocessing
   */
  
  #include <arpa/inet.h>
  #ifdef __MACH__
  #include <machine/endian.h>
  #else
  #include <endian.h>
  #endif
  #include <inttypes.h>
  #include <string.h>
  #include <stdlib.h>
  
  #include <ogg/ogg.h>
  
  #include "pp-opus.h"
  #include "pp-opus-silence.h"
  #include "../debug.h"
  #include "../version.h"
  
  
  /* OGG/Opus helpers */
  FILE *ogg_file = NULL;
  ogg_stream_state *stream = NULL;
  
  void le32(unsigned char *p, int v);
  void le16(unsigned char *p, int v);
  ogg_packet *op_opushead(void);
  ogg_packet *op_opustags(char *metadata);
  ogg_packet *op_from_pkt(const unsigned char *pkt, int len);
  void op_free(ogg_packet *op);
  int ogg_write(void);
  int ogg_flush(void);
  
  
  int janus_pp_opus_create(char *destination, char *metadata) {
  	stream = g_malloc0(sizeof(ogg_stream_state));
  	if(ogg_stream_init(stream, rand()) < 0) {
  		JANUS_LOG(LOG_ERR, "Couldn't initialize Ogg stream state\n");
  		return -1;
  	}
  	ogg_file = fopen(destination, "wb");
  	if(ogg_file == NULL) {
  		JANUS_LOG(LOG_ERR, "Couldn't open output file\n");
  		return -1;
  	}
  	JANUS_LOG(LOG_INFO, "Writing .opus file header\n");
  	/* Write stream headers */
  	ogg_packet *op = op_opushead();
  	ogg_stream_packetin(stream, op);
  	op_free(op);
  	op = op_opustags(metadata);
  	ogg_stream_packetin(stream, op);
  	op_free(op);
  	ogg_flush();
  	return 0;
  }
  
  int janus_pp_opus_process(FILE *file, janus_pp_frame_packet *list, int *working) {
  	if(!file || !list || !working)
  		return -1;
  	janus_pp_frame_packet *tmp = list;
  	long int offset = 0;
  	int bytes = 0, len = 0, steps = 0, last_seq = 0;
  	uint64_t pos = 0;
  	uint8_t *buffer = g_malloc0(1500);
  	while(*working && tmp != NULL) {
  		if(tmp->prev != NULL && ((tmp->ts - tmp->prev->ts)/48/20 > 1)) {
  			JANUS_LOG(LOG_WARN, "Lost a packet here? (got seq %"SCNu16" after %"SCNu16", time ~%"SCNu64"s)\n",
  				tmp->seq, tmp->prev->seq, (tmp->ts-list->ts)/48000);
  			/* FIXME Write the silence packet N times to fill in the gaps */
  			ogg_packet *op = op_from_pkt((const unsigned char *)opus_silence, sizeof(opus_silence));
  			/* use ts differ to insert silence packet */
  			int silence_count = (tmp->ts - tmp->prev->ts)/48/20 - 1;
  			pos = (tmp->prev->ts - list->ts) / 48 / 20 + 1;
  			JANUS_LOG(LOG_WARN, "[FILL] pos: %06"SCNu64", writing silences (count=%d)\n", pos, silence_count);
  			int i=0;
  			for(i=0; i<silence_count; i++) {
  				pos = (tmp->prev->ts - list->ts) / 48 / 20 + i + 1;
  				op->granulepos = 960*(pos); /* FIXME: get this from the toc byte */
  				ogg_stream_packetin(stream, op);
  				ogg_write();
  			}
  			ogg_flush();
  			g_free(op);
  		}
  		if(tmp->drop) {
  			/* We marked this packet as one to drop, before */
  			JANUS_LOG(LOG_WARN, "Dropping previously marked audio packet (time ~%"SCNu64"s)\n", (tmp->ts-list->ts)/48000);
  			tmp = tmp->next;
  			continue;
  		}
  		if(tmp->audiolevel != -1) {
  			JANUS_LOG(LOG_VERB, "Audio level: %d dB\n", tmp->audiolevel);
  		}
  		guint16 diff = tmp->prev == NULL ? 1 : (tmp->seq - tmp->prev->seq);
  		len = 0;
  		/* RTP payload */
  		offset = tmp->offset+12+tmp->skip;
  		fseek(file, offset, SEEK_SET);
  		len = tmp->len-12-tmp->skip;
  		if(len < 1) {
  			tmp = tmp->next;
  			continue;
  		}
  		bytes = fread(buffer, sizeof(char), len, file);
  		if(bytes != len) {
  			JANUS_LOG(LOG_WARN, "Didn't manage to read all the bytes we needed (%d < %d)...\n", bytes, len);
  			tmp = tmp->next;
  			continue;
  		}
  		if(last_seq == 0)
  			last_seq = tmp->seq;
  		if(tmp->seq < last_seq) {
  			last_seq = tmp->seq;
  			steps++;
  		}
  		ogg_packet *op = op_from_pkt((const unsigned char *)buffer, bytes);
  		pos = (tmp->ts - list->ts) / 48 / 20 + 1;
  		JANUS_LOG(LOG_VERB, "pos: %06"SCNu64", writing %d bytes out of %d (seq=%"SCNu16", step=%"SCNu16", ts=%"SCNu64", time=%"SCNu64"s)\n",
  			pos, bytes, tmp->len, tmp->seq, diff, tmp->ts, (tmp->ts-list->ts)/48000);
  		op->granulepos = 960*(pos); /* FIXME: get this from the toc byte */
  		ogg_stream_packetin(stream, op);
  		g_free(op);
  		ogg_write();
  		ogg_flush();
  		tmp = tmp->next;
  	}
  	g_free(buffer);
  	return 0;
  }
  
  void janus_pp_opus_close(void) {
  	ogg_flush();
  	if(ogg_file)
  		fclose(ogg_file);
  	ogg_file = NULL;
  	if(stream)
  		ogg_stream_destroy(stream);
  	stream = NULL;
  }
  
  
  /* OGG/Opus helpers */
  /* Write a little-endian 32 bit int to memory */
  void le32(unsigned char *p, int v) {
  	p[0] = v & 0xff;
  	p[1] = (v >> 8) & 0xff;
  	p[2] = (v >> 16) & 0xff;
  	p[3] = (v >> 24) & 0xff;
  }
  
  
  /* Write a little-endian 16 bit int to memory */
  void le16(unsigned char *p, int v) {
  	p[0] = v & 0xff;
  	p[1] = (v >> 8) & 0xff;
  }
  
  /* Manufacture a generic OpusHead packet */
  ogg_packet *op_opushead(void) {
  	int size = 19;
  	unsigned char *data = g_malloc(size);
  	ogg_packet *op = g_malloc(sizeof(*op));
  
  	memcpy(data, "OpusHead", 8);  /* identifier */
  	data[8] = 1;                  /* version */
  	data[9] = 2;                  /* channels */
  	le16(data+10, 0);             /* pre-skip */
  	le32(data + 12, 48000);       /* original sample rate */
  	le16(data + 16, 0);           /* gain */
  	data[18] = 0;                 /* channel mapping family */
  
  	op->packet = data;
  	op->bytes = size;
  	op->b_o_s = 1;
  	op->e_o_s = 0;
  	op->granulepos = 0;
  	op->packetno = 0;
  
  	return op;
  }
  
  /* Manufacture a generic OpusTags packet */
  ogg_packet *op_opustags(char *metadata) {
  	const char *identifier = "OpusTags";
  	const char *desc = "DESCRIPTION=";
  	char vendor[256];
  	g_snprintf(vendor, sizeof(vendor), "Janus post-processor %s", janus_version_string);
  	int size = strlen(identifier) + 4 + strlen(vendor) + 4;
  	int dlen = strlen(desc), mlen = metadata ? strlen(metadata) : 0;
  	if(mlen > 0)
  		size += (4+dlen+mlen);
  	unsigned char *data = g_malloc(size);
  	ogg_packet *op = g_malloc(sizeof(*op));
  
  	/* Write down the tags */
  	memcpy(data, identifier, 8);
  	le32(data + 8, strlen(vendor));
  	memcpy(data + 12, vendor, strlen(vendor));
  	le32(data + 12 + strlen(vendor), mlen > 0 ? 1 : 0);
  	/* Check if we have metadata to write down: we'll use the "DESCRIPTION" tag */
  	if(metadata && strlen(metadata) > 0) {
  		/* Add a single comment */
  		le32(data + 12 + strlen(vendor) + 4, dlen+mlen);
  		memcpy(data + 12 + strlen(vendor) + 8, desc, dlen);
  		memcpy(data + 12 + strlen(vendor) + 8 + dlen, metadata, mlen);
  	}
  
  	op->packet = data;
  	op->bytes = size;
  	op->b_o_s = 0;
  	op->e_o_s = 0;
  	op->granulepos = 0;
  	op->packetno = 1;
  
  	return op;
  }
  
  /* Allocate an ogg_packet */
  ogg_packet *op_from_pkt(const unsigned char *pkt, int len) {
  	ogg_packet *op = g_malloc(sizeof(*op));
  
  	op->packet = (unsigned char *)pkt;
  	op->bytes = len;
  	op->b_o_s = 0;
  	op->e_o_s = 0;
  	op->granulepos = 0;
  	op->packetno = 0;
  
  	return op;
  }
  
  /* Free a packet and its contents */
  void op_free(ogg_packet *op) {
  	if(op) {
  		if(op->packet) {
  			g_free(op->packet);
  		}
  		g_free(op);
  	}
  }
  
  /* Write out available ogg pages */
  int ogg_write(void) {
  	ogg_page page;
  	size_t written;
  
  	if(!stream || !ogg_file) {
  		return -1;
  	}
  
  	while (ogg_stream_pageout(stream, &page)) {
  		written = fwrite(page.header, 1, page.header_len, ogg_file);
  		if(written != (size_t)page.header_len) {
  			JANUS_LOG(LOG_ERR, "Error writing Ogg page header\n");
  			return -2;
  		}
  		written = fwrite(page.body, 1, page.body_len, ogg_file);
  		if(written != (size_t)page.body_len) {
  			JANUS_LOG(LOG_ERR, "Error writing Ogg page body\n");
  			return -3;
  		}
  	}
  	return 0;
  }
  
  /* Flush remaining ogg data */
  int ogg_flush(void) {
  	ogg_page page;
  	size_t written;
  
  	if(!stream || !ogg_file) {
  		return -1;
  	}
  
  	while (ogg_stream_flush(stream, &page)) {
  		written = fwrite(page.header, 1, page.header_len, ogg_file);
  		if(written != (size_t)page.header_len) {
  			JANUS_LOG(LOG_ERR, "Error writing Ogg page header\n");
  			return -2;
  		}
  		written = fwrite(page.body, 1, page.body_len, ogg_file);
  		if(written != (size_t)page.body_len) {
  			JANUS_LOG(LOG_ERR, "Error writing Ogg page body\n");
  			return -3;
  		}
  	}
  	return 0;
  }
  