421 lines
10 KiB
C++
421 lines
10 KiB
C++
/*
|
|
* Copyright (C) 2003-2013 The Music Player Daemon Project
|
|
* http://www.musicpd.org
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#include "config.h"
|
|
#include "OpusEncoderPlugin.hxx"
|
|
#include "OggStream.hxx"
|
|
#include "OggSerial.hxx"
|
|
#include "EncoderAPI.hxx"
|
|
#include "AudioFormat.hxx"
|
|
#include "ConfigError.hxx"
|
|
#include "util/Error.hxx"
|
|
#include "util/Domain.hxx"
|
|
#include "system/ByteOrder.hxx"
|
|
|
|
#include <opus.h>
|
|
#include <ogg/ogg.h>
|
|
|
|
#include <glib.h>
|
|
|
|
#include <assert.h>
|
|
#include <stdlib.h>
|
|
|
|
struct opus_encoder {
|
|
/** the base class */
|
|
Encoder encoder;
|
|
|
|
/* configuration */
|
|
|
|
opus_int32 bitrate;
|
|
int complexity;
|
|
int signal;
|
|
|
|
/* runtime information */
|
|
|
|
AudioFormat audio_format;
|
|
|
|
size_t frame_size;
|
|
|
|
size_t buffer_frames, buffer_size, buffer_position;
|
|
uint8_t *buffer;
|
|
|
|
OpusEncoder *enc;
|
|
|
|
unsigned char buffer2[1275 * 3 + 7];
|
|
|
|
OggStream stream;
|
|
|
|
int lookahead;
|
|
|
|
ogg_int64_t packetno;
|
|
|
|
ogg_int64_t granulepos;
|
|
|
|
opus_encoder():encoder(opus_encoder_plugin) {}
|
|
};
|
|
|
|
static constexpr Domain opus_encoder_domain("opus_encoder");
|
|
|
|
static bool
|
|
opus_encoder_configure(struct opus_encoder *encoder,
|
|
const config_param ¶m, Error &error)
|
|
{
|
|
const char *value = param.GetBlockValue("bitrate", "auto");
|
|
if (strcmp(value, "auto") == 0)
|
|
encoder->bitrate = OPUS_AUTO;
|
|
else if (strcmp(value, "max") == 0)
|
|
encoder->bitrate = OPUS_BITRATE_MAX;
|
|
else {
|
|
char *endptr;
|
|
encoder->bitrate = strtoul(value, &endptr, 10);
|
|
if (endptr == value || *endptr != 0 ||
|
|
encoder->bitrate < 500 || encoder->bitrate > 512000) {
|
|
error.Set(config_domain, "Invalid bit rate");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
encoder->complexity = param.GetBlockValue("complexity", 10u);
|
|
if (encoder->complexity > 10) {
|
|
error.Format(config_domain, "Invalid complexity");
|
|
return false;
|
|
}
|
|
|
|
value = param.GetBlockValue("signal", "auto");
|
|
if (strcmp(value, "auto") == 0)
|
|
encoder->signal = OPUS_AUTO;
|
|
else if (strcmp(value, "voice") == 0)
|
|
encoder->signal = OPUS_SIGNAL_VOICE;
|
|
else if (strcmp(value, "music") == 0)
|
|
encoder->signal = OPUS_SIGNAL_MUSIC;
|
|
else {
|
|
error.Format(config_domain, "Invalid signal");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static Encoder *
|
|
opus_encoder_init(const config_param ¶m, Error &error)
|
|
{
|
|
opus_encoder *encoder = new opus_encoder();
|
|
|
|
/* load configuration from "param" */
|
|
if (!opus_encoder_configure(encoder, param, error)) {
|
|
/* configuration has failed, roll back and return error */
|
|
delete encoder;
|
|
return NULL;
|
|
}
|
|
|
|
return &encoder->encoder;
|
|
}
|
|
|
|
static void
|
|
opus_encoder_finish(Encoder *_encoder)
|
|
{
|
|
struct opus_encoder *encoder = (struct opus_encoder *)_encoder;
|
|
|
|
/* the real libopus cleanup was already performed by
|
|
opus_encoder_close(), so no real work here */
|
|
delete encoder;
|
|
}
|
|
|
|
static bool
|
|
opus_encoder_open(Encoder *_encoder,
|
|
AudioFormat &audio_format,
|
|
Error &error)
|
|
{
|
|
struct opus_encoder *encoder = (struct opus_encoder *)_encoder;
|
|
|
|
/* libopus supports only 48 kHz */
|
|
audio_format.sample_rate = 48000;
|
|
|
|
if (audio_format.channels > 2)
|
|
audio_format.channels = 1;
|
|
|
|
switch (audio_format.format) {
|
|
case SampleFormat::S16:
|
|
case SampleFormat::FLOAT:
|
|
break;
|
|
|
|
case SampleFormat::S8:
|
|
audio_format.format = SampleFormat::S16;
|
|
break;
|
|
|
|
default:
|
|
audio_format.format = SampleFormat::FLOAT;
|
|
break;
|
|
}
|
|
|
|
encoder->audio_format = audio_format;
|
|
encoder->frame_size = audio_format.GetFrameSize();
|
|
|
|
int error_code;
|
|
encoder->enc = opus_encoder_create(audio_format.sample_rate,
|
|
audio_format.channels,
|
|
OPUS_APPLICATION_AUDIO,
|
|
&error_code);
|
|
if (encoder->enc == nullptr) {
|
|
error.Set(opus_encoder_domain, error_code,
|
|
opus_strerror(error_code));
|
|
return false;
|
|
}
|
|
|
|
opus_encoder_ctl(encoder->enc, OPUS_SET_BITRATE(encoder->bitrate));
|
|
opus_encoder_ctl(encoder->enc,
|
|
OPUS_SET_COMPLEXITY(encoder->complexity));
|
|
opus_encoder_ctl(encoder->enc, OPUS_SET_SIGNAL(encoder->signal));
|
|
|
|
opus_encoder_ctl(encoder->enc, OPUS_GET_LOOKAHEAD(&encoder->lookahead));
|
|
|
|
encoder->buffer_frames = audio_format.sample_rate / 50;
|
|
encoder->buffer_size = encoder->frame_size * encoder->buffer_frames;
|
|
encoder->buffer_position = 0;
|
|
encoder->buffer = (unsigned char *)g_malloc(encoder->buffer_size);
|
|
|
|
encoder->stream.Initialize(GenerateOggSerial());
|
|
encoder->packetno = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
opus_encoder_close(Encoder *_encoder)
|
|
{
|
|
struct opus_encoder *encoder = (struct opus_encoder *)_encoder;
|
|
|
|
encoder->stream.Deinitialize();
|
|
g_free(encoder->buffer);
|
|
opus_encoder_destroy(encoder->enc);
|
|
}
|
|
|
|
static bool
|
|
opus_encoder_do_encode(struct opus_encoder *encoder, bool eos,
|
|
Error &error)
|
|
{
|
|
assert(encoder->buffer_position == encoder->buffer_size);
|
|
|
|
opus_int32 result =
|
|
encoder->audio_format.format == SampleFormat::S16
|
|
? opus_encode(encoder->enc,
|
|
(const opus_int16 *)encoder->buffer,
|
|
encoder->buffer_frames,
|
|
encoder->buffer2,
|
|
sizeof(encoder->buffer2))
|
|
: opus_encode_float(encoder->enc,
|
|
(const float *)encoder->buffer,
|
|
encoder->buffer_frames,
|
|
encoder->buffer2,
|
|
sizeof(encoder->buffer2));
|
|
if (result < 0) {
|
|
error.Set(opus_encoder_domain, "Opus encoder error");
|
|
return false;
|
|
}
|
|
|
|
encoder->granulepos += encoder->buffer_frames;
|
|
|
|
ogg_packet packet;
|
|
packet.packet = encoder->buffer2;
|
|
packet.bytes = result;
|
|
packet.b_o_s = false;
|
|
packet.e_o_s = eos;
|
|
packet.granulepos = encoder->granulepos;
|
|
packet.packetno = encoder->packetno++;
|
|
encoder->stream.PacketIn(packet);
|
|
|
|
encoder->buffer_position = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
opus_encoder_end(Encoder *_encoder, Error &error)
|
|
{
|
|
struct opus_encoder *encoder = (struct opus_encoder *)_encoder;
|
|
|
|
encoder->stream.Flush();
|
|
|
|
memset(encoder->buffer + encoder->buffer_position, 0,
|
|
encoder->buffer_size - encoder->buffer_position);
|
|
encoder->buffer_position = encoder->buffer_size;
|
|
|
|
return opus_encoder_do_encode(encoder, true, error);
|
|
}
|
|
|
|
static bool
|
|
opus_encoder_flush(Encoder *_encoder, gcc_unused Error &error)
|
|
{
|
|
struct opus_encoder *encoder = (struct opus_encoder *)_encoder;
|
|
|
|
encoder->stream.Flush();
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
opus_encoder_write_silence(struct opus_encoder *encoder, unsigned fill_frames,
|
|
Error &error)
|
|
{
|
|
size_t fill_bytes = fill_frames * encoder->frame_size;
|
|
|
|
while (fill_bytes > 0) {
|
|
size_t nbytes =
|
|
encoder->buffer_size - encoder->buffer_position;
|
|
if (nbytes > fill_bytes)
|
|
nbytes = fill_bytes;
|
|
|
|
memset(encoder->buffer + encoder->buffer_position,
|
|
0, nbytes);
|
|
encoder->buffer_position += nbytes;
|
|
fill_bytes -= nbytes;
|
|
|
|
if (encoder->buffer_position == encoder->buffer_size &&
|
|
!opus_encoder_do_encode(encoder, false, error))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
opus_encoder_write(Encoder *_encoder,
|
|
const void *_data, size_t length,
|
|
Error &error)
|
|
{
|
|
struct opus_encoder *encoder = (struct opus_encoder *)_encoder;
|
|
const uint8_t *data = (const uint8_t *)_data;
|
|
|
|
if (encoder->lookahead > 0) {
|
|
/* generate some silence at the beginning of the
|
|
stream */
|
|
|
|
assert(encoder->buffer_position == 0);
|
|
|
|
if (!opus_encoder_write_silence(encoder, encoder->lookahead,
|
|
error))
|
|
return false;
|
|
|
|
encoder->lookahead = 0;
|
|
}
|
|
|
|
while (length > 0) {
|
|
size_t nbytes =
|
|
encoder->buffer_size - encoder->buffer_position;
|
|
if (nbytes > length)
|
|
nbytes = length;
|
|
|
|
memcpy(encoder->buffer + encoder->buffer_position,
|
|
data, nbytes);
|
|
data += nbytes;
|
|
length -= nbytes;
|
|
encoder->buffer_position += nbytes;
|
|
|
|
if (encoder->buffer_position == encoder->buffer_size &&
|
|
!opus_encoder_do_encode(encoder, false, error))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
opus_encoder_generate_head(struct opus_encoder *encoder)
|
|
{
|
|
unsigned char header[19];
|
|
memcpy(header, "OpusHead", 8);
|
|
header[8] = 1;
|
|
header[9] = encoder->audio_format.channels;
|
|
*(uint16_t *)(header + 10) = ToLE16(encoder->lookahead);
|
|
*(uint32_t *)(header + 12) =
|
|
ToLE32(encoder->audio_format.sample_rate);
|
|
header[16] = 0;
|
|
header[17] = 0;
|
|
header[18] = 0;
|
|
|
|
ogg_packet packet;
|
|
packet.packet = header;
|
|
packet.bytes = 19;
|
|
packet.b_o_s = true;
|
|
packet.e_o_s = false;
|
|
packet.granulepos = 0;
|
|
packet.packetno = encoder->packetno++;
|
|
encoder->stream.PacketIn(packet);
|
|
encoder->stream.Flush();
|
|
}
|
|
|
|
static void
|
|
opus_encoder_generate_tags(struct opus_encoder *encoder)
|
|
{
|
|
const char *version = opus_get_version_string();
|
|
size_t version_length = strlen(version);
|
|
|
|
size_t comments_size = 8 + 4 + version_length + 4;
|
|
unsigned char *comments = (unsigned char *)g_malloc(comments_size);
|
|
memcpy(comments, "OpusTags", 8);
|
|
*(uint32_t *)(comments + 8) = ToLE32(version_length);
|
|
memcpy(comments + 12, version, version_length);
|
|
*(uint32_t *)(comments + 12 + version_length) = ToLE32(0);
|
|
|
|
ogg_packet packet;
|
|
packet.packet = comments;
|
|
packet.bytes = comments_size;
|
|
packet.b_o_s = false;
|
|
packet.e_o_s = false;
|
|
packet.granulepos = 0;
|
|
packet.packetno = encoder->packetno++;
|
|
encoder->stream.PacketIn(packet);
|
|
encoder->stream.Flush();
|
|
|
|
g_free(comments);
|
|
}
|
|
|
|
static size_t
|
|
opus_encoder_read(Encoder *_encoder, void *dest, size_t length)
|
|
{
|
|
struct opus_encoder *encoder = (struct opus_encoder *)_encoder;
|
|
|
|
if (encoder->packetno == 0)
|
|
opus_encoder_generate_head(encoder);
|
|
else if (encoder->packetno == 1)
|
|
opus_encoder_generate_tags(encoder);
|
|
|
|
return encoder->stream.PageOut(dest, length);
|
|
}
|
|
|
|
static const char *
|
|
opus_encoder_get_mime_type(gcc_unused Encoder *_encoder)
|
|
{
|
|
return "audio/ogg";
|
|
}
|
|
|
|
const EncoderPlugin opus_encoder_plugin = {
|
|
"opus",
|
|
opus_encoder_init,
|
|
opus_encoder_finish,
|
|
opus_encoder_open,
|
|
opus_encoder_close,
|
|
opus_encoder_end,
|
|
opus_encoder_flush,
|
|
nullptr,
|
|
nullptr,
|
|
opus_encoder_write,
|
|
opus_encoder_read,
|
|
opus_encoder_get_mime_type,
|
|
};
|