diff options
Diffstat (limited to 'src/main/jni/audio.c')
-rw-r--r-- | src/main/jni/audio.c | 671 |
1 files changed, 671 insertions, 0 deletions
diff --git a/src/main/jni/audio.c b/src/main/jni/audio.c new file mode 100644 index 000000000..3ca0f1fb2 --- /dev/null +++ b/src/main/jni/audio.c @@ -0,0 +1,671 @@ +#include <jni.h> +#include <ogg/ogg.h> +#include <stdio.h> +#include <opus.h> +#include <stdlib.h> +#include <time.h> +#include <opusfile.h> +#include "utils.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; + +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 + int vendor_length = strlen(vendor_string); + int user_comment_list_length = 0; + int 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, int 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 + int newlen = (*length + amount + 255) / 255 * 255 - 1; + p = realloc(p, newlen); + for (int 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), page->header_len, os); + written += fwrite(page->body, sizeof(unsigned char), page->body_len, os); + return written; +} + +const opus_int32 bitrate = 16000; +const opus_int32 rate = 16000; +const opus_int32 frame_size = 960; +const int with_cvbr = 1; +const int max_ogg_delay = 0; +const int comment_padding = 512; + +opus_int32 coding_rate = 16000; +ogg_int32_t _packetId; +OpusEncoder *_encoder = 0; +uint8_t *_packet = 0; +ogg_stream_state os; +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; + +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; + 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) { + cleanupRecorder(); + + if (!path) { + return 0; + } + + _fileOs = fopen(path, "wb"); + if (!_fileOs) { + return 0; + } + + inopt.rate = rate; + inopt.gain = 0; + inopt.endianness = 0; + inopt.copy_comments = 0; + inopt.rawmode = 1; + inopt.ignorelength = 1; + inopt.samplesize = 16; + inopt.channels = 1; + inopt.skip = 0; + + comment_init(&inopt.comments, &inopt.comments_length, opus_get_version_string()); + + if (rate > 24000) { + coding_rate = 48000; + } else if (rate > 16000) { + coding_rate = 24000; + } else if (rate > 12000) { + coding_rate = 16000; + } else if (rate > 8000) { + coding_rate = 12000; + } else { + coding_rate = 8000; + } + + 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_AUDIO, &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)); + 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, 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; +} + +int writeFrame(uint8_t *framePcmBytes, unsigned int frameByteCount) { + int 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); + paddedFrameBytes = NULL; + } + + 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 = (unsigned char *)_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 int Java_org_telegram_android_MediaController_startRecord(JNIEnv *env, jclass class, jstring path) { + const char *pathStr = (*env)->GetStringUTFChars(env, path, 0); + + int result = initRecorder(pathStr); + + if (pathStr != 0) { + (*env)->ReleaseStringUTFChars(env, path, pathStr); + } + + return result; +} + +JNIEXPORT int Java_org_telegram_android_MediaController_writeFrame(JNIEnv *env, jclass class, jobject frame, jint len) { + jbyte *frameBytes = (*env)->GetDirectBufferAddress(env, frame); + return writeFrame(frameBytes, len); +} + +JNIEXPORT void Java_org_telegram_android_MediaController_stopRecord(JNIEnv *env, jclass class) { + cleanupRecorder(); +} + +//player +OggOpusFile *_opusFile; +int _isSeekable = 0; +int64_t _totalPcmDuration = 0; +int64_t _currentPcmOffset = 0; +int _finished = 0; +static const int playerBuffersCount = 3; +static const int playerSampleRate = 48000; + +void cleanupPlayer() { + if (_opusFile) { + op_free(_opusFile); + _opusFile = 0; + } + _isSeekable = 0; + _totalPcmDuration = 0; + _currentPcmOffset = 0; + _finished = 0; +} + +int seekPlayer(float position) { + if (!_opusFile || !_isSeekable || position < 0) { + return 0; + } + int result = op_pcm_seek(_opusFile, (ogg_int64_t)(position * _totalPcmDuration)); + if (result != OPUS_OK) { + LOGE("op_pcm_seek failed: %d", result); + } + ogg_int64_t pcmPosition = op_pcm_tell(_opusFile); + _currentPcmOffset = pcmPosition; + return result == OPUS_OK; +} + +int initPlayer(const char *path) { + cleanupPlayer(); + + int openError = OPUS_OK; + _opusFile = op_open_file(path, &openError); + if (!_opusFile || openError != OPUS_OK) { + LOGE("op_open_file failed: %d", openError); + cleanupPlayer(); + return 0; + } + + _isSeekable = op_seekable(_opusFile); + _totalPcmDuration = op_pcm_total(_opusFile, -1); + + return 1; +} + +void fillBuffer(uint8_t *buffer, int capacity, int *args) { + if (_opusFile) { + args[1] = max(0, op_pcm_tell(_opusFile)); + + if (_finished) { + args[0] = 0; + args[1] = 0; + args[2] = 1; + return; + } else { + int writtenOutputBytes = 0; + int endOfFileReached = 0; + + while (writtenOutputBytes < capacity) { + int readSamples = op_read(_opusFile, (opus_int16 *)(buffer + writtenOutputBytes), (capacity - writtenOutputBytes) / 2, NULL); + + if (readSamples > 0) { + writtenOutputBytes += readSamples * 2; + } else { + if (readSamples < 0) { + LOGE("op_read failed: %d", readSamples); + } + endOfFileReached = 1; + break; + } + } + + args[0] = writtenOutputBytes; + + if (endOfFileReached || args[1] + args[0] == _totalPcmDuration) { + _finished = 1; + args[2] = 1; + } else { + args[2] = 0; + } + } + } else { + memset(buffer, 0, capacity); + args[0] = capacity; + args[1] = _totalPcmDuration; + } +} + +JNIEXPORT jlong Java_org_telegram_android_MediaController_getTotalPcmDuration(JNIEnv *env, jclass class) { + return _totalPcmDuration; +} + +JNIEXPORT void Java_org_telegram_android_MediaController_readOpusFile(JNIEnv *env, jclass class, jobject buffer, jint capacity, jintArray args) { + jint *argsArr = (*env)->GetIntArrayElements(env, args, 0); + jbyte *bufferBytes = (*env)->GetDirectBufferAddress(env, buffer); + fillBuffer(bufferBytes, capacity, argsArr); + (*env)->ReleaseIntArrayElements(env, args, argsArr, 0); +} + +JNIEXPORT int Java_org_telegram_android_MediaController_seekOpusFile(JNIEnv *env, jclass class, jfloat position) { + return seekPlayer(position); +} + +JNIEXPORT int Java_org_telegram_android_MediaController_openOpusFile(JNIEnv *env, jclass class, jstring path) { + const char *pathStr = (*env)->GetStringUTFChars(env, path, 0); + + int result = initPlayer(pathStr); + + if (pathStr != 0) { + (*env)->ReleaseStringUTFChars(env, path, pathStr); + } + + return result; +} + +JNIEXPORT void Java_org_telegram_android_MediaController_closeOpusFile(JNIEnv *env, jclass class) { + cleanupPlayer(); +} + +JNIEXPORT int Java_org_telegram_android_MediaController_isOpusFile(JNIEnv *env, jclass class, jstring path) { + const char *pathStr = (*env)->GetStringUTFChars(env, path, 0); + + int result = 0; + + int error = OPUS_OK; + OggOpusFile *file = op_test_file(pathStr, &error); + if (file != NULL) { + int error = op_test_open(file); + op_free(file); + + result = error == OPUS_OK; + } + + if (pathStr != 0) { + (*env)->ReleaseStringUTFChars(env, path, pathStr); + } + + return result; +} |