999 lines
29 KiB
C
999 lines
29 KiB
C
#include <jni.h>
|
|
#include <ogg/ogg.h>
|
|
#include <stdio.h>
|
|
#include <opus.h>
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
#include <opusfile.h>
|
|
#include <math.h>
|
|
#include "c_utils.h"
|
|
#include "libavformat/avformat.h"
|
|
|
|
typedef struct {
|
|
int version;
|
|
int channels; /* Number of channels: 1..255 */
|
|
int preskip;
|
|
ogg_uint32_t input_sample_rate;
|
|
int gain; /* in dB S7.8 should be zero whenever possible */
|
|
int channel_mapping;
|
|
/* The rest is only used if channel_mapping != 0 */
|
|
int nb_streams;
|
|
int nb_coupled;
|
|
unsigned char stream_map[255];
|
|
} OpusHeader;
|
|
|
|
typedef struct {
|
|
unsigned char *data;
|
|
int maxlen;
|
|
int pos;
|
|
} Packet;
|
|
|
|
typedef struct {
|
|
const unsigned char *data;
|
|
int maxlen;
|
|
int pos;
|
|
} ROPacket;
|
|
|
|
typedef struct {
|
|
void *readdata;
|
|
opus_int64 total_samples_per_channel;
|
|
int rawmode;
|
|
int channels;
|
|
long rate;
|
|
int gain;
|
|
int samplesize;
|
|
int endianness;
|
|
char *infilename;
|
|
int ignorelength;
|
|
int skip;
|
|
int extraout;
|
|
char *comments;
|
|
int comments_length;
|
|
int copy_comments;
|
|
} oe_enc_opt;
|
|
|
|
typedef struct {
|
|
ogg_int32_t _packetId;
|
|
opus_int64 bytes_written;
|
|
opus_int64 pages_out;
|
|
opus_int64 total_samples;
|
|
ogg_int64_t enc_granulepos;
|
|
int size_segments;
|
|
int last_segments;
|
|
ogg_int64_t last_granulepos;
|
|
opus_int32 min_bytes;
|
|
int max_frame_bytes;
|
|
int serialno;
|
|
} resume_data;
|
|
|
|
static int write_uint32(Packet *p, ogg_uint32_t val) {
|
|
if (p->pos > p->maxlen - 4) {
|
|
return 0;
|
|
}
|
|
p->data[p->pos ] = (val ) & 0xFF;
|
|
p->data[p->pos+1] = (val>> 8) & 0xFF;
|
|
p->data[p->pos+2] = (val>>16) & 0xFF;
|
|
p->data[p->pos+3] = (val>>24) & 0xFF;
|
|
p->pos += 4;
|
|
return 1;
|
|
}
|
|
|
|
static int write_uint16(Packet *p, ogg_uint16_t val) {
|
|
if (p->pos > p->maxlen-2) {
|
|
return 0;
|
|
}
|
|
p->data[p->pos ] = (val ) & 0xFF;
|
|
p->data[p->pos+1] = (val>> 8) & 0xFF;
|
|
p->pos += 2;
|
|
return 1;
|
|
}
|
|
|
|
static int write_chars(Packet *p, const unsigned char *str, int nb_chars)
|
|
{
|
|
int i;
|
|
if (p->pos>p->maxlen-nb_chars)
|
|
return 0;
|
|
for (i=0;i<nb_chars;i++)
|
|
p->data[p->pos++] = str[i];
|
|
return 1;
|
|
}
|
|
|
|
static int read_uint32(ROPacket *p, ogg_uint32_t *val)
|
|
{
|
|
if (p->pos>p->maxlen-4)
|
|
return 0;
|
|
*val = (ogg_uint32_t)p->data[p->pos ];
|
|
*val |= (ogg_uint32_t)p->data[p->pos+1]<< 8;
|
|
*val |= (ogg_uint32_t)p->data[p->pos+2]<<16;
|
|
*val |= (ogg_uint32_t)p->data[p->pos+3]<<24;
|
|
p->pos += 4;
|
|
return 1;
|
|
}
|
|
|
|
static int read_uint16(ROPacket *p, ogg_uint16_t *val)
|
|
{
|
|
if (p->pos>p->maxlen-2)
|
|
return 0;
|
|
*val = (ogg_uint16_t)p->data[p->pos ];
|
|
*val |= (ogg_uint16_t)p->data[p->pos+1]<<8;
|
|
p->pos += 2;
|
|
return 1;
|
|
}
|
|
|
|
static int read_chars(ROPacket *p, unsigned char *str, int nb_chars)
|
|
{
|
|
int i;
|
|
if (p->pos>p->maxlen-nb_chars)
|
|
return 0;
|
|
for (i=0;i<nb_chars;i++)
|
|
str[i] = p->data[p->pos++];
|
|
return 1;
|
|
}
|
|
|
|
int opus_header_to_packet(const OpusHeader *h, unsigned char *packet, int len) {
|
|
int i;
|
|
Packet p;
|
|
unsigned char ch;
|
|
|
|
p.data = packet;
|
|
p.maxlen = len;
|
|
p.pos = 0;
|
|
if (len < 19) {
|
|
return 0;
|
|
}
|
|
if (!write_chars(&p, (const unsigned char *)"OpusHead", 8)) {
|
|
return 0;
|
|
}
|
|
|
|
ch = 1;
|
|
if (!write_chars(&p, &ch, 1)) {
|
|
return 0;
|
|
}
|
|
|
|
ch = h->channels;
|
|
if (!write_chars(&p, &ch, 1)) {
|
|
return 0;
|
|
}
|
|
|
|
if (!write_uint16(&p, h->preskip)) {
|
|
return 0;
|
|
}
|
|
|
|
if (!write_uint32(&p, h->input_sample_rate)) {
|
|
return 0;
|
|
}
|
|
|
|
if (!write_uint16(&p, h->gain)) {
|
|
return 0;
|
|
}
|
|
|
|
ch = h->channel_mapping;
|
|
if (!write_chars(&p, &ch, 1)) {
|
|
return 0;
|
|
}
|
|
|
|
if (h->channel_mapping != 0) {
|
|
ch = h->nb_streams;
|
|
if (!write_chars(&p, &ch, 1)) {
|
|
return 0;
|
|
}
|
|
|
|
ch = h->nb_coupled;
|
|
if (!write_chars(&p, &ch, 1)) {
|
|
return 0;
|
|
}
|
|
|
|
/* Multi-stream support */
|
|
for (i = 0; i < h->channels; i++) {
|
|
if (!write_chars(&p, &h->stream_map[i], 1)) {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return p.pos;
|
|
}
|
|
|
|
#define writeint(buf, base, val) do { buf[base + 3] = ((val) >> 24) & 0xff; \
|
|
buf[base + 2]=((val) >> 16) & 0xff; \
|
|
buf[base + 1]=((val) >> 8) & 0xff; \
|
|
buf[base] = (val) & 0xff; \
|
|
} while(0)
|
|
|
|
static void comment_init(char **comments, int *length, const char *vendor_string) {
|
|
// The 'vendor' field should be the actual encoding library used
|
|
size_t vendor_length = strlen(vendor_string);
|
|
int user_comment_list_length = 0;
|
|
size_t len = 8 + 4 + vendor_length + 4;
|
|
char *p = (char *)malloc(len);
|
|
memcpy(p, "OpusTags", 8);
|
|
writeint(p, 8, vendor_length);
|
|
memcpy(p + 12, vendor_string, vendor_length);
|
|
writeint(p, 12 + vendor_length, user_comment_list_length);
|
|
*length = len;
|
|
*comments = p;
|
|
}
|
|
|
|
static void comment_pad(char **comments, int* length, size_t amount) {
|
|
if (amount > 0) {
|
|
char *p = *comments;
|
|
// Make sure there is at least amount worth of padding free, and round up to the maximum that fits in the current ogg segments
|
|
size_t newlen = (*length + amount + 255) / 255 * 255 - 1;
|
|
p = realloc(p, newlen);
|
|
for (int32_t i = *length; i < newlen; i++) {
|
|
p[i] = 0;
|
|
}
|
|
*comments = p;
|
|
*length = newlen;
|
|
}
|
|
}
|
|
|
|
static int writeOggPage(ogg_page *page, FILE *os) {
|
|
int written = fwrite(page->header, sizeof(unsigned char), (size_t) page->header_len, os);
|
|
written += fwrite(page->body, sizeof(unsigned char), (size_t) page->body_len, os);
|
|
return written;
|
|
}
|
|
|
|
const opus_int32 bitrate = OPUS_BITRATE_MAX;
|
|
const opus_int32 frame_size = 960;
|
|
const int with_cvbr = 1;
|
|
const int max_ogg_delay = 0;
|
|
const int comment_padding = 512;
|
|
|
|
opus_int32 rate = 48000;
|
|
opus_int32 coding_rate = 48000;
|
|
|
|
ogg_int32_t _packetId;
|
|
OpusEncoder *_encoder = 0;
|
|
uint8_t *_packet = 0;
|
|
ogg_stream_state os;
|
|
const char *_filePath;
|
|
FILE *_fileOs = 0;
|
|
oe_enc_opt inopt;
|
|
OpusHeader header;
|
|
opus_int32 min_bytes;
|
|
int max_frame_bytes;
|
|
ogg_packet op;
|
|
ogg_page og;
|
|
opus_int64 bytes_written;
|
|
opus_int64 pages_out;
|
|
opus_int64 total_samples;
|
|
ogg_int64_t enc_granulepos;
|
|
ogg_int64_t last_granulepos;
|
|
int size_segments;
|
|
int last_segments;
|
|
int serialno;
|
|
|
|
void cleanupRecorder() {
|
|
|
|
ogg_stream_flush(&os, &og);
|
|
|
|
if (_encoder) {
|
|
opus_encoder_destroy(_encoder);
|
|
_encoder = 0;
|
|
}
|
|
|
|
ogg_stream_clear(&os);
|
|
|
|
if (_packet) {
|
|
free(_packet);
|
|
_packet = 0;
|
|
}
|
|
|
|
if (_fileOs) {
|
|
fclose(_fileOs);
|
|
_fileOs = 0;
|
|
}
|
|
|
|
_packetId = -1;
|
|
bytes_written = 0;
|
|
pages_out = 0;
|
|
total_samples = 0;
|
|
enc_granulepos = 0;
|
|
size_segments = 0;
|
|
last_segments = 0;
|
|
last_granulepos = 0;
|
|
if (_filePath) {
|
|
free(_filePath);
|
|
_filePath = 0;
|
|
}
|
|
memset(&os, 0, sizeof(ogg_stream_state));
|
|
memset(&inopt, 0, sizeof(oe_enc_opt));
|
|
memset(&header, 0, sizeof(OpusHeader));
|
|
memset(&op, 0, sizeof(ogg_packet));
|
|
memset(&og, 0, sizeof(ogg_page));
|
|
}
|
|
|
|
int initRecorder(const char *path, opus_int32 sampleRate) {
|
|
cleanupRecorder();
|
|
|
|
coding_rate = sampleRate;
|
|
rate = sampleRate;
|
|
|
|
if (!path) {
|
|
LOGE("path is null");
|
|
return 0;
|
|
}
|
|
|
|
int length = strlen(path);
|
|
_filePath = (char*) malloc(length + 1);
|
|
strcpy(_filePath, path);
|
|
|
|
_fileOs = fopen(path, "w");
|
|
if (!_fileOs) {
|
|
LOGE("error cannot open file: %s", path);
|
|
return 0;
|
|
}
|
|
|
|
inopt.rate = rate;
|
|
inopt.gain = 0;
|
|
inopt.endianness = 0;
|
|
inopt.copy_comments = 0;
|
|
inopt.rawmode = 0;
|
|
inopt.ignorelength = 0;
|
|
inopt.samplesize = 16;
|
|
inopt.channels = 1;
|
|
inopt.skip = 0;
|
|
|
|
comment_init(&inopt.comments, &inopt.comments_length, opus_get_version_string());
|
|
|
|
if (rate != coding_rate) {
|
|
LOGE("Invalid rate");
|
|
return 0;
|
|
}
|
|
|
|
header.channels = 1;
|
|
header.channel_mapping = 0;
|
|
header.input_sample_rate = rate;
|
|
header.gain = inopt.gain;
|
|
header.nb_streams = 1;
|
|
|
|
int result = OPUS_OK;
|
|
_encoder = opus_encoder_create(coding_rate, 1, OPUS_APPLICATION_VOIP, &result);
|
|
if (result != OPUS_OK) {
|
|
LOGE("Error cannot create encoder: %s", opus_strerror(result));
|
|
return 0;
|
|
}
|
|
|
|
min_bytes = max_frame_bytes = (1275 * 3 + 7) * header.nb_streams;
|
|
_packet = malloc(max_frame_bytes);
|
|
|
|
result = opus_encoder_ctl(_encoder, OPUS_SET_BITRATE(bitrate));
|
|
//result = opus_encoder_ctl(_encoder, OPUS_SET_COMPLEXITY(10));
|
|
if (result != OPUS_OK) {
|
|
LOGE("Error OPUS_SET_BITRATE returned: %s", opus_strerror(result));
|
|
return 0;
|
|
}
|
|
|
|
#ifdef OPUS_SET_LSB_DEPTH
|
|
result = opus_encoder_ctl(_encoder, OPUS_SET_LSB_DEPTH(MAX(8, MIN(24, inopt.samplesize))));
|
|
if (result != OPUS_OK) {
|
|
LOGE("Warning OPUS_SET_LSB_DEPTH returned: %s", opus_strerror(result));
|
|
}
|
|
#endif
|
|
|
|
opus_int32 lookahead;
|
|
result = opus_encoder_ctl(_encoder, OPUS_GET_LOOKAHEAD(&lookahead));
|
|
if (result != OPUS_OK) {
|
|
LOGE("Error OPUS_GET_LOOKAHEAD returned: %s", opus_strerror(result));
|
|
return 0;
|
|
}
|
|
|
|
inopt.skip += lookahead;
|
|
header.preskip = (int)(inopt.skip * (48000.0 / coding_rate));
|
|
inopt.extraout = (int)(header.preskip * (rate / 48000.0));
|
|
|
|
if (ogg_stream_init(&os, serialno = rand()) == -1) {
|
|
LOGE("Error: stream init failed");
|
|
return 0;
|
|
}
|
|
|
|
unsigned char header_data[100];
|
|
int packet_size = opus_header_to_packet(&header, header_data, 100);
|
|
op.packet = header_data;
|
|
op.bytes = packet_size;
|
|
op.b_o_s = 1;
|
|
op.e_o_s = 0;
|
|
op.granulepos = 0;
|
|
op.packetno = 0;
|
|
ogg_stream_packetin(&os, &op);
|
|
|
|
while ((result = ogg_stream_flush(&os, &og))) {
|
|
if (!result) {
|
|
break;
|
|
}
|
|
|
|
int pageBytesWritten = writeOggPage(&og, _fileOs);
|
|
if (pageBytesWritten != og.header_len + og.body_len) {
|
|
LOGE("Error: failed writing header to output stream");
|
|
return 0;
|
|
}
|
|
bytes_written += pageBytesWritten;
|
|
pages_out++;
|
|
}
|
|
|
|
comment_pad(&inopt.comments, &inopt.comments_length, comment_padding);
|
|
op.packet = (unsigned char *)inopt.comments;
|
|
op.bytes = inopt.comments_length;
|
|
op.b_o_s = 0;
|
|
op.e_o_s = 0;
|
|
op.granulepos = 0;
|
|
op.packetno = 1;
|
|
ogg_stream_packetin(&os, &op);
|
|
|
|
while ((result = ogg_stream_flush(&os, &og))) {
|
|
if (result == 0) {
|
|
break;
|
|
}
|
|
|
|
int writtenPageBytes = writeOggPage(&og, _fileOs);
|
|
if (writtenPageBytes != og.header_len + og.body_len) {
|
|
LOGE("Error: failed writing header to output stream");
|
|
return 0;
|
|
}
|
|
|
|
bytes_written += writtenPageBytes;
|
|
pages_out++;
|
|
}
|
|
|
|
free(inopt.comments);
|
|
|
|
return 1;
|
|
}
|
|
|
|
void saveResumeData() {
|
|
if (_filePath == NULL) {
|
|
return;
|
|
}
|
|
const char* ext = ".resume";
|
|
char* _resumeFilePath = (char*) malloc(strlen(_filePath) + strlen(ext) + 1);
|
|
strcpy(_resumeFilePath, _filePath);
|
|
strcat(_resumeFilePath, ext);
|
|
|
|
FILE* resumeFile = fopen(_resumeFilePath, "wb");
|
|
if (!resumeFile) {
|
|
LOGE("error cannot open resume file to write: %s", _resumeFilePath);
|
|
free(_resumeFilePath);
|
|
return;
|
|
}
|
|
resume_data data;
|
|
data._packetId = _packetId;
|
|
data.bytes_written = bytes_written;
|
|
data.pages_out = pages_out;
|
|
data.total_samples = total_samples;
|
|
data.enc_granulepos = enc_granulepos;
|
|
data.size_segments = size_segments;
|
|
data.last_segments = last_segments;
|
|
data.last_granulepos = last_granulepos;
|
|
data.min_bytes = min_bytes;
|
|
data.max_frame_bytes = max_frame_bytes;
|
|
data.serialno = serialno;
|
|
|
|
if (fwrite(&data, sizeof(resume_data), 1, resumeFile) != 1) {
|
|
LOGE("error writing resume data to file: %s", _resumeFilePath);
|
|
}
|
|
fclose(resumeFile);
|
|
|
|
free(_resumeFilePath);
|
|
}
|
|
|
|
resume_data readResumeData(const char* filePath) {
|
|
|
|
const char* ext = ".resume";
|
|
char* _resumeFilePath = (char*) malloc(strlen(filePath) + strlen(ext) + 1);
|
|
strcpy(_resumeFilePath, filePath);
|
|
strcat(_resumeFilePath, ext);
|
|
|
|
resume_data data;
|
|
|
|
FILE* resumeFile = fopen(_resumeFilePath, "rb");
|
|
if (!resumeFile) {
|
|
LOGE("error cannot open resume file to read: %s", _resumeFilePath);
|
|
memset(&data, 0, sizeof(resume_data));
|
|
return data;
|
|
}
|
|
|
|
if (fread(&data, sizeof(resume_data), 1, resumeFile) != 1) {
|
|
LOGE("error cannot read resume file: %s", _resumeFilePath);
|
|
memset(&data, 0, sizeof(resume_data));
|
|
}
|
|
|
|
fclose(resumeFile);
|
|
free(_resumeFilePath);
|
|
|
|
return data;
|
|
}
|
|
|
|
int resumeRecorder(const char *path, opus_int32 sampleRate) {
|
|
cleanupRecorder();
|
|
|
|
coding_rate = sampleRate;
|
|
rate = sampleRate;
|
|
|
|
if (!path) {
|
|
LOGE("path is null");
|
|
return 0;
|
|
}
|
|
|
|
int length = strlen(path);
|
|
_filePath = (char*) malloc(length + 1);
|
|
strcpy(_filePath, path);
|
|
|
|
resume_data resumeData = readResumeData(path);
|
|
_packetId = resumeData._packetId;
|
|
bytes_written = resumeData.bytes_written;
|
|
pages_out = resumeData.pages_out;
|
|
total_samples = resumeData.total_samples;
|
|
enc_granulepos = resumeData.enc_granulepos;
|
|
size_segments = resumeData.size_segments;
|
|
last_segments = resumeData.last_segments;
|
|
last_granulepos = resumeData.last_granulepos;
|
|
min_bytes = resumeData.min_bytes;
|
|
max_frame_bytes = resumeData.max_frame_bytes;
|
|
serialno = resumeData.serialno;
|
|
|
|
_fileOs = fopen(path, "a");
|
|
if (!_fileOs) {
|
|
LOGE("error cannot open resume file: %s", path);
|
|
return 0;
|
|
}
|
|
|
|
int result = OPUS_OK;
|
|
_encoder = opus_encoder_create(coding_rate, 1, OPUS_APPLICATION_VOIP, &result);
|
|
if (result != OPUS_OK) {
|
|
LOGE("Error cannot create encoder: %s", opus_strerror(result));
|
|
return 0;
|
|
}
|
|
|
|
_packet = malloc(max_frame_bytes);
|
|
|
|
result = opus_encoder_ctl(_encoder, OPUS_SET_BITRATE(bitrate));
|
|
//result = opus_encoder_ctl(_encoder, OPUS_SET_COMPLEXITY(10));
|
|
if (result != OPUS_OK) {
|
|
LOGE("Error OPUS_SET_BITRATE returned: %s", opus_strerror(result));
|
|
return 0;
|
|
}
|
|
|
|
#ifdef OPUS_SET_LSB_DEPTH
|
|
result = opus_encoder_ctl(_encoder, OPUS_SET_LSB_DEPTH(MAX(8, MIN(24, 16))));
|
|
if (result != OPUS_OK) {
|
|
LOGE("Warning OPUS_SET_LSB_DEPTH returned: %s", opus_strerror(result));
|
|
}
|
|
#endif
|
|
|
|
if (ogg_stream_init(&os, serialno) == -1) {
|
|
LOGE("Error: stream init failed");
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
int writeFrame(uint8_t *framePcmBytes, uint32_t frameByteCount) {
|
|
size_t cur_frame_size = frame_size;
|
|
_packetId++;
|
|
|
|
opus_int32 nb_samples = frameByteCount / 2;
|
|
total_samples += nb_samples;
|
|
if (nb_samples < frame_size) {
|
|
op.e_o_s = 1;
|
|
} else {
|
|
op.e_o_s = 0;
|
|
}
|
|
|
|
int nbBytes = 0;
|
|
|
|
if (nb_samples != 0) {
|
|
uint8_t *paddedFrameBytes = framePcmBytes;
|
|
int freePaddedFrameBytes = 0;
|
|
|
|
if (nb_samples < cur_frame_size) {
|
|
paddedFrameBytes = malloc(cur_frame_size * 2);
|
|
freePaddedFrameBytes = 1;
|
|
memcpy(paddedFrameBytes, framePcmBytes, frameByteCount);
|
|
memset(paddedFrameBytes + nb_samples * 2, 0, cur_frame_size * 2 - nb_samples * 2);
|
|
}
|
|
|
|
nbBytes = opus_encode(_encoder, (opus_int16 *)paddedFrameBytes, cur_frame_size, _packet, max_frame_bytes / 10);
|
|
if (freePaddedFrameBytes) {
|
|
free(paddedFrameBytes);
|
|
}
|
|
|
|
if (nbBytes < 0) {
|
|
LOGE("Encoding failed: %s. Aborting.", opus_strerror(nbBytes));
|
|
return 0;
|
|
}
|
|
|
|
enc_granulepos += cur_frame_size * 48000 / coding_rate;
|
|
size_segments = (nbBytes + 255) / 255;
|
|
min_bytes = MIN(nbBytes, min_bytes);
|
|
}
|
|
|
|
while ((((size_segments <= 255) && (last_segments + size_segments > 255)) || (enc_granulepos - last_granulepos > max_ogg_delay)) && ogg_stream_flush_fill(&os, &og, 255 * 255)) {
|
|
if (ogg_page_packets(&og) != 0) {
|
|
last_granulepos = ogg_page_granulepos(&og);
|
|
}
|
|
|
|
last_segments -= og.header[26];
|
|
int writtenPageBytes = writeOggPage(&og, _fileOs);
|
|
if (writtenPageBytes != og.header_len + og.body_len) {
|
|
LOGE("Error: failed writing data to output stream");
|
|
return 0;
|
|
}
|
|
bytes_written += writtenPageBytes;
|
|
pages_out++;
|
|
}
|
|
|
|
op.packet = _packet;
|
|
op.bytes = nbBytes;
|
|
op.b_o_s = 0;
|
|
op.granulepos = enc_granulepos;
|
|
if (op.e_o_s) {
|
|
op.granulepos = ((total_samples * 48000 + rate - 1) / rate) + header.preskip;
|
|
}
|
|
op.packetno = 2 + _packetId;
|
|
ogg_stream_packetin(&os, &op);
|
|
last_segments += size_segments;
|
|
|
|
while ((op.e_o_s || (enc_granulepos + (frame_size * 48000 / coding_rate) - last_granulepos > max_ogg_delay) || (last_segments >= 255)) ? ogg_stream_flush_fill(&os, &og, 255 * 255) : ogg_stream_pageout_fill(&os, &og, 255 * 255)) {
|
|
if (ogg_page_packets(&og) != 0) {
|
|
last_granulepos = ogg_page_granulepos(&og);
|
|
}
|
|
last_segments -= og.header[26];
|
|
int writtenPageBytes = writeOggPage(&og, _fileOs);
|
|
if (writtenPageBytes != og.header_len + og.body_len) {
|
|
LOGE("Error: failed writing data to output stream");
|
|
return 0;
|
|
}
|
|
bytes_written += writtenPageBytes;
|
|
pages_out++;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
JNIEXPORT jint Java_org_telegram_messenger_MediaController_startRecord(JNIEnv *env, jclass class, jstring path, jint sampleRate) {
|
|
const char *pathStr = (*env)->GetStringUTFChars(env, path, 0);
|
|
|
|
int32_t result = initRecorder(pathStr, sampleRate);
|
|
|
|
if (pathStr != 0) {
|
|
(*env)->ReleaseStringUTFChars(env, path, pathStr);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
JNIEXPORT jint Java_org_telegram_messenger_MediaController_resumeRecord(JNIEnv *env, jclass class, jstring path, jint sampleRate) {
|
|
const char *pathStr = (*env)->GetStringUTFChars(env, path, 0);
|
|
|
|
int32_t result = resumeRecorder(pathStr, sampleRate);
|
|
|
|
if (pathStr != 0) {
|
|
(*env)->ReleaseStringUTFChars(env, path, pathStr);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
JNIEXPORT jint Java_org_telegram_messenger_MediaController_writeFrame(JNIEnv *env, jclass class, jobject frame, jint len) {
|
|
jbyte *frameBytes = (*env)->GetDirectBufferAddress(env, frame);
|
|
return writeFrame((uint8_t *) frameBytes, (uint32_t) len);
|
|
}
|
|
|
|
JNIEXPORT void Java_org_telegram_messenger_MediaController_stopRecord(JNIEnv *env, jclass class, jboolean allowResuming) {
|
|
if (allowResuming && _filePath != NULL) {
|
|
saveResumeData();
|
|
}
|
|
cleanupRecorder();
|
|
}
|
|
|
|
JNIEXPORT jint Java_org_telegram_messenger_MediaController_isOpusFile(JNIEnv *env, jclass class, jstring path) {
|
|
const char *pathStr = (*env)->GetStringUTFChars(env, path, 0);
|
|
|
|
int32_t result = 0;
|
|
|
|
int32_t error = OPUS_OK;
|
|
OggOpusFile *file = op_test_file(pathStr, &error);
|
|
if (file != NULL) {
|
|
error = op_test_open(file);
|
|
op_free(file);
|
|
result = error == OPUS_OK;
|
|
}
|
|
|
|
if (pathStr != 0) {
|
|
(*env)->ReleaseStringUTFChars(env, path, pathStr);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static inline void set_bits(uint8_t *bytes, int32_t bitOffset, int32_t value) {
|
|
bytes += bitOffset / 8;
|
|
bitOffset %= 8;
|
|
*((int32_t *) bytes) |= (value << bitOffset);
|
|
}
|
|
|
|
JNIEXPORT jbyteArray Java_org_telegram_messenger_MediaController_getWaveform2(JNIEnv *env, jclass class, jshortArray array, jint length) {
|
|
|
|
jshort *sampleBuffer = (*env)->GetShortArrayElements(env, array, 0);
|
|
|
|
const int32_t resultSamples = 100;
|
|
uint16_t *samples = malloc(100 * 2);
|
|
uint64_t sampleIndex = 0;
|
|
uint16_t peakSample = 0;
|
|
int32_t sampleRate = (int32_t) MAX(1, length / resultSamples);
|
|
int32_t index = 0;
|
|
|
|
for (int32_t i = 0; i < length; i++) {
|
|
uint16_t sample = (uint16_t) abs(sampleBuffer[i]);
|
|
if (sample > peakSample) {
|
|
peakSample = sample;
|
|
}
|
|
if (sampleIndex++ % sampleRate == 0) {
|
|
if (index < resultSamples) {
|
|
samples[index++] = peakSample;
|
|
}
|
|
peakSample = 0;
|
|
}
|
|
}
|
|
|
|
int64_t sumSamples = 0;
|
|
for (int32_t i = 0; i < resultSamples; i++) {
|
|
sumSamples += samples[i];
|
|
}
|
|
uint16_t peak = (uint16_t) (sumSamples * 1.8f / resultSamples);
|
|
if (peak < 2500) {
|
|
peak = 2500;
|
|
}
|
|
|
|
for (int32_t i = 0; i < resultSamples; i++) {
|
|
uint16_t sample = (uint16_t) ((int64_t) samples[i]);
|
|
if (sample > peak) {
|
|
samples[i] = peak;
|
|
}
|
|
}
|
|
|
|
(*env)->ReleaseShortArrayElements(env, array, sampleBuffer, 0);
|
|
|
|
uint32_t bitstreamLength = resultSamples * 5 / 8 + 1;
|
|
jbyteArray *result = (*env)->NewByteArray(env, bitstreamLength);
|
|
if (result) {
|
|
uint8_t *bytes = malloc(bitstreamLength + 4);
|
|
memset(bytes, 0, bitstreamLength + 4);
|
|
for (int32_t i = 0; i < resultSamples; i++) {
|
|
int32_t value = MIN(31, abs((int32_t) samples[i]) * 31 / peak);
|
|
set_bits(bytes, i * 5, value & 31);
|
|
}
|
|
(*env)->SetByteArrayRegion(env, result, 0, bitstreamLength, (jbyte *) bytes);
|
|
}
|
|
free(samples);
|
|
|
|
return result;
|
|
}
|
|
|
|
int16_t *sampleBuffer = NULL;
|
|
|
|
JNIEXPORT jbyteArray Java_org_telegram_messenger_MediaController_getWaveform(JNIEnv *env, jclass class, jstring path) {
|
|
const char *pathStr = (*env)->GetStringUTFChars(env, path, 0);
|
|
jbyteArray result = 0;
|
|
|
|
int error = OPUS_OK;
|
|
OggOpusFile *opusFile = op_open_file(pathStr, &error);
|
|
if (opusFile != NULL && error == OPUS_OK) {
|
|
int64_t totalSamples = op_pcm_total(opusFile, -1);
|
|
const uint32_t resultSamples = 100;
|
|
int32_t sampleRate = MAX(1, (int32_t) (totalSamples / resultSamples));
|
|
|
|
uint16_t *samples = malloc(100 * 2);
|
|
|
|
size_t bufferSize = 1024 * 128;
|
|
if (sampleBuffer == NULL) {
|
|
sampleBuffer = malloc(bufferSize);
|
|
}
|
|
uint64_t sampleIndex = 0;
|
|
uint16_t peakSample = 0;
|
|
|
|
int32_t index = 0;
|
|
|
|
while (1) {
|
|
int readSamples = op_read(opusFile, sampleBuffer, bufferSize / 2, NULL);
|
|
for (int32_t i = 0; i < readSamples; i++) {
|
|
uint16_t sample = (uint16_t) abs(sampleBuffer[i]);
|
|
if (sample > peakSample) {
|
|
peakSample = sample;
|
|
}
|
|
if (sampleIndex++ % sampleRate == 0) {
|
|
if (index < resultSamples) {
|
|
samples[index++] = peakSample;
|
|
}
|
|
peakSample = 0;
|
|
}
|
|
}
|
|
if (readSamples == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
int64_t sumSamples = 0;
|
|
for (int32_t i = 0; i < resultSamples; i++) {
|
|
sumSamples += samples[i];
|
|
}
|
|
uint16_t peak = (uint16_t) (sumSamples * 1.8f / resultSamples);
|
|
if (peak < 2500) {
|
|
peak = 2500;
|
|
}
|
|
|
|
for (int32_t i = 0; i < resultSamples; i++) {
|
|
uint16_t sample = (uint16_t) ((int64_t) samples[i]);
|
|
if (sample > peak) {
|
|
samples[i] = peak;
|
|
}
|
|
}
|
|
|
|
//free(sampleBuffer);
|
|
op_free(opusFile);
|
|
|
|
uint32_t bitstreamLength = (resultSamples * 5) / 8 + 1;
|
|
result = (*env)->NewByteArray(env, bitstreamLength);
|
|
if (result) {
|
|
uint8_t *bytes = malloc(bitstreamLength + 4);
|
|
memset(bytes, 0, bitstreamLength + 4);
|
|
|
|
for (int32_t i = 0; i < resultSamples; i++) {
|
|
int32_t value = MIN(31, abs((int32_t) samples[i]) * 31 / peak);
|
|
set_bits(bytes, i * 5, value & 31);
|
|
}
|
|
|
|
(*env)->SetByteArrayRegion(env, result, 0, bitstreamLength, (jbyte *) bytes);
|
|
}
|
|
free(samples);
|
|
}
|
|
|
|
if (pathStr != 0) {
|
|
(*env)->ReleaseStringUTFChars(env, path, pathStr);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
JNIEXPORT void JNICALL Java_org_telegram_ui_Stories_recorder_FfmpegAudioWaveformLoader_init(JNIEnv *env, jobject obj, jstring pathJStr, jint count) {
|
|
const char *path = (*env)->GetStringUTFChars(env, pathJStr, 0);
|
|
|
|
// Initialize FFmpeg components
|
|
av_register_all();
|
|
|
|
AVFormatContext *formatContext = avformat_alloc_context();
|
|
if (!formatContext) {
|
|
// Handle error
|
|
return;
|
|
}
|
|
|
|
int res;
|
|
if ((res = avformat_open_input(&formatContext, path, NULL, NULL)) != 0) {
|
|
LOGD("avformat_open_input error %s", av_err2str(res));
|
|
// Handle error
|
|
avformat_free_context(formatContext);
|
|
return;
|
|
}
|
|
|
|
if (avformat_find_stream_info(formatContext, NULL) < 0) {
|
|
// Handle error
|
|
avformat_close_input(&formatContext);
|
|
return;
|
|
}
|
|
|
|
AVCodec *codec = NULL;
|
|
int audioStreamIndex = av_find_best_stream(formatContext, AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0);
|
|
if (audioStreamIndex < 0) {
|
|
LOGD("av_find_best_stream error %s", av_err2str(audioStreamIndex));
|
|
// Handle error
|
|
avformat_close_input(&formatContext);
|
|
return;
|
|
}
|
|
|
|
AVCodecContext *codecContext = avcodec_alloc_context3(codec);
|
|
avcodec_parameters_to_context(codecContext, formatContext->streams[audioStreamIndex]->codecpar);
|
|
|
|
int64_t duration_in_microseconds = formatContext->duration;
|
|
double duration_in_seconds = (double)duration_in_microseconds / AV_TIME_BASE;
|
|
|
|
if (avcodec_open2(codecContext, codec, NULL) < 0) {
|
|
// Handle error
|
|
avcodec_free_context(&codecContext);
|
|
avformat_close_input(&formatContext);
|
|
return;
|
|
}
|
|
|
|
// Obtain the class and method to callback
|
|
jclass cls = (*env)->GetObjectClass(env, obj);
|
|
jmethodID mid = (*env)->GetMethodID(env, cls, "receiveChunk", "([SI)V");
|
|
|
|
AVFrame *frame = av_frame_alloc();
|
|
AVPacket packet;
|
|
|
|
int sampleRate = codecContext->sample_rate; // Sample rate from FFmpeg's codec context
|
|
int skip = 4;
|
|
int barWidth = (int) round((double) duration_in_seconds * sampleRate / count / (1 + skip)); // Assuming you have 'duration' and 'count' defined somewhere
|
|
|
|
short peak = 0;
|
|
int currentCount = 0;
|
|
int index = 0;
|
|
int chunkIndex = 0;
|
|
short waveformChunkData[32]; // Allocate the chunk array
|
|
|
|
while (av_read_frame(formatContext, &packet) >= 0) {
|
|
if (packet.stream_index == audioStreamIndex) {
|
|
// Decode the audio packet
|
|
int response = avcodec_send_packet(codecContext, &packet);
|
|
|
|
while (response >= 0) {
|
|
response = avcodec_receive_frame(codecContext, frame);
|
|
if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
|
|
break;
|
|
} else if (response < 0) {
|
|
// Handle error
|
|
break;
|
|
}
|
|
|
|
int16_t* samples = (int16_t*) frame->data[0];
|
|
for (int i = 0; i < frame->nb_samples; i++) {
|
|
short value = samples[i]; // Read the 16-bit PCM sample
|
|
|
|
if (currentCount >= barWidth) {
|
|
waveformChunkData[index - chunkIndex] = peak;
|
|
index++;
|
|
if (index - chunkIndex >= sizeof(waveformChunkData) / sizeof(short) || index >= count) {
|
|
jshortArray waveformData = (*env)->NewShortArray(env, sizeof(waveformChunkData) / sizeof(short));
|
|
(*env)->SetShortArrayRegion(env, waveformData, 0, sizeof(waveformChunkData) / sizeof(short), waveformChunkData);
|
|
(*env)->CallVoidMethod(env, obj, mid, waveformData, sizeof(waveformChunkData) / sizeof(short));
|
|
|
|
// Reset the chunk data
|
|
memset(waveformChunkData, 0, sizeof(waveformChunkData));
|
|
chunkIndex = index;
|
|
|
|
// Delete local reference to avoid memory leak
|
|
(*env)->DeleteLocalRef(env, waveformData);
|
|
}
|
|
peak = 0;
|
|
currentCount = 0;
|
|
if (index >= count) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (peak < value) {
|
|
peak = value;
|
|
}
|
|
currentCount++;
|
|
|
|
// Skip logic
|
|
i += skip;
|
|
if (i >= frame->nb_samples) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
av_packet_unref(&packet);
|
|
|
|
if (index >= count) {
|
|
break;
|
|
}
|
|
|
|
// Check for stopping flag
|
|
jfieldID fid = (*env)->GetFieldID(env, cls, "running", "Z");
|
|
jboolean running = (*env)->GetBooleanField(env, obj, fid);
|
|
if (running == JNI_FALSE) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
av_frame_free(&frame);
|
|
avcodec_free_context(&codecContext);
|
|
avformat_close_input(&formatContext);
|
|
|
|
(*env)->ReleaseStringUTFChars(env, pathJStr, path);
|
|
} |