ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
audio.cpp
Go to the documentation of this file.
1#include <emscripten/bind.h>
2extern "C" {
4#include "../client.h"
5}
6
7#include <cstddef>
8#include <cstdint>
9#include <string>
10#include <vector>
11
15EMSCRIPTEN_DECLARE_VAL_TYPE(NullableOpusBytes);
16EMSCRIPTEN_DECLARE_VAL_TYPE(NullablePcmSamples);
17
18static audio_processor_t *create_processor(int sample_rate, bool bypass, unsigned frame_samples, int channels) {
19 if (sample_rate <= 0 || frame_samples == 0 || (channels != 1 && channels != 2)) {
20 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid browser audio processor configuration");
21 return nullptr;
22 }
24 config.channels = channels;
25 if (bypass) {
26 config.spectral_enabled = false;
27 config.flags = {};
28 }
29 audio_processor_t *processor = nullptr;
30 return audio_processor_create(&config, &processor) == ASCIICHAT_OK ? processor : nullptr;
31}
32
34public:
35 AudioProcessorBinding(int sample_rate, bool bypass, unsigned frame_samples)
36 : AudioProcessorBinding(sample_rate, bypass, frame_samples, 2) {}
37 AudioProcessorBinding(int sample_rate, bool bypass, unsigned frame_samples, int channels)
38 : processor_(create_processor(sample_rate, bypass, frame_samples, channels)),
39 scratch_(processor_ ? frame_samples : 0) {}
41 if (processor_)
42 audio_processor_destroy(processor_);
43 }
44
47
48 bool valid() const {
49 return processor_ != nullptr;
50 }
51 int process(AudioSamples input, AudioSamples output) {
52 const size_t count = input["length"].as<size_t>();
53 if (!processor_)
54 return SET_ERRNO(ERROR_INVALID_STATE, "Audio processor is not initialized");
55 if (count == 0 || count > scratch_.size() || output["length"].as<size_t>() != count)
56 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid audio processor buffer size: %zu", count);
57 emscripten::val view = emscripten::val(emscripten::typed_memory_view(count, scratch_.data()));
58 view.call<void>("set", input);
59 int result = audio_processor_process(processor_, scratch_.data(), scratch_.data(), count);
60 if (result == 0) {
61 // Processing may grow memory, so do not retain the previous JS view.
62 emscripten::val result_view = emscripten::val(emscripten::typed_memory_view(count, scratch_.data()));
63 output.call<void>("set", result_view);
64 }
65 return result;
66 }
67
68private:
69 audio_processor *processor_;
70 std::vector<float> scratch_;
71};
72
73static bool opus_codec_in_use = false;
74
75// The underlying client codec is module-local. Scratch is allocated before
76// audio callbacks and retained for the lifetime of this codec.
78public:
79 OpusCodecBinding(int sample_rate, int channels, int bitrate, unsigned max_packet) : channels_(channels) {
80 if (opus_codec_in_use) {
81 SET_ERRNO(ERROR_AUDIO_IN_USE, "Browser Opus codec is already in use");
82 return;
83 }
84 if (sample_rate <= 0 || sample_rate > 48000 || (channels != 1 && channels != 2) || !max_packet) {
85 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid Opus codec configuration");
86 return;
87 }
88 if (client_opus_encoder_init(sample_rate, channels, bitrate) != 0) {
89 SET_ERRNO(ERROR_AUDIO, "Cannot initialize browser Opus encoder");
90 return;
91 }
92 if (client_opus_decoder_init(sample_rate, channels) != 0) {
94 SET_ERRNO(ERROR_AUDIO, "Cannot initialize browser Opus decoder");
95 return;
96 }
97 const size_t frame = (static_cast<size_t>(sample_rate) * 12 + 99) / 100;
98 pcm_.resize(frame * channels);
99 encoded_.resize(max_packet);
100 input_.resize(65535);
101 initialized_ = true;
102 opus_codec_in_use = true;
103 }
105 if (initialized_) {
108 opus_codec_in_use = false;
109 }
110 }
113 bool valid() const {
114 return initialized_;
115 }
116
117 NullableOpusBytes encode(PcmSamples samples) {
118 const size_t count = samples["length"].as<size_t>();
119 if (!initialized_) {
120 SET_ERRNO(ERROR_INVALID_STATE, "Opus codec is not initialized");
121 return NullableOpusBytes(emscripten::val::null());
122 }
123 if (!count || count > pcm_.size() || count % channels_) {
124 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid Opus input sample count: %zu", count);
125 return NullableOpusBytes(emscripten::val::null());
126 }
127 emscripten::val view = emscripten::val(emscripten::typed_memory_view(count, pcm_.data()));
128 view.call<void>("set", samples);
129 const int size = client_opus_encode(pcm_.data(), static_cast<int>(count / channels_), encoded_.data(),
130 static_cast<int>(encoded_.size()));
131 if (size < 0) {
132 SET_ERRNO(ERROR_AUDIO, "Browser Opus encoding failed");
133 return NullableOpusBytes(emscripten::val::null());
134 }
135 emscripten::val result = emscripten::val::global("Uint8Array").new_(size);
136 result.call<void>("set", emscripten::val(emscripten::typed_memory_view(size, encoded_.data())));
137 return NullableOpusBytes(result);
138 }
139 NullablePcmSamples decode(OpusBytes packet, int frame_size) {
140 const size_t count = packet["length"].as<size_t>();
141 if (!initialized_) {
142 SET_ERRNO(ERROR_INVALID_STATE, "Opus codec is not initialized");
143 return NullablePcmSamples(emscripten::val::null());
144 }
145 if (count > input_.size() || frame_size <= 0 || static_cast<size_t>(frame_size) * channels_ > pcm_.size()) {
146 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid Opus packet or output frame size");
147 return NullablePcmSamples(emscripten::val::null());
148 }
149 if (count)
150 emscripten::val(emscripten::typed_memory_view(count, input_.data())).call<void>("set", packet);
151 const int samples = client_opus_decode(input_.data(), static_cast<int>(count), pcm_.data(), frame_size, 0);
152 if (samples < 0) {
153 SET_ERRNO(ERROR_AUDIO, "Browser Opus decoding failed");
154 return NullablePcmSamples(emscripten::val::null());
155 }
156 const size_t output_count = static_cast<size_t>(samples) * channels_;
157 emscripten::val result = emscripten::val::global("Int16Array").new_(output_count);
158 result.call<void>("set", emscripten::val(emscripten::typed_memory_view(output_count, pcm_.data())));
159 return NullablePcmSamples(result);
160 }
161
162private:
163 int channels_;
164 bool initialized_ = false;
165 std::vector<int16_t> pcm_;
166 std::vector<uint8_t> encoded_, input_;
167};
168
169EMSCRIPTEN_BINDINGS(ascii_chat_audio) {
170 emscripten::register_type<AudioSamples>("AudioSamples", "Float32Array");
171 emscripten::register_type<PcmSamples>("PcmSamples", "Int16Array");
172 emscripten::register_type<OpusBytes>("OpusBytes", "Uint8Array");
173 emscripten::register_type<NullableOpusBytes>("NullableOpusBytes", "Uint8Array | null");
174 emscripten::register_type<NullablePcmSamples>("NullablePcmSamples", "Int16Array | null");
175 emscripten::class_<AudioProcessorBinding>("AudioProcessorBinding")
176 .constructor<int, bool, unsigned>()
177 .constructor<int, bool, unsigned, int>()
178 .function("valid", &AudioProcessorBinding::valid)
179 .function("process", &AudioProcessorBinding::process);
180 emscripten::class_<OpusCodecBinding>("OpusCodecBinding")
181 .constructor<int, int, int, unsigned>()
182 .function("valid", &OpusCodecBinding::valid)
183 .function("encode", &OpusCodecBinding::encode)
184 .function("decode", &OpusCodecBinding::decode);
185}
#define SET_ERRNO(code,...)
EMSCRIPTEN_DECLARE_VAL_TYPE(AudioSamples)
EMSCRIPTEN_BINDINGS(ascii_chat_audio)
Definition audio.cpp:169
AudioProcessorBinding(const AudioProcessorBinding &)=delete
AudioProcessorBinding & operator=(const AudioProcessorBinding &)=delete
AudioProcessorBinding(int sample_rate, bool bypass, unsigned frame_samples, int channels)
Definition audio.cpp:37
AudioProcessorBinding(int sample_rate, bool bypass, unsigned frame_samples)
Definition audio.cpp:35
int process(AudioSamples input, AudioSamples output)
Definition audio.cpp:51
bool valid() const
Definition audio.cpp:48
bool valid() const
Definition audio.cpp:113
OpusCodecBinding(int sample_rate, int channels, int bitrate, unsigned max_packet)
Definition audio.cpp:79
NullablePcmSamples decode(OpusBytes packet, int frame_size)
Definition audio.cpp:139
OpusCodecBinding & operator=(const OpusCodecBinding &)=delete
NullableOpusBytes encode(PcmSamples samples)
Definition audio.cpp:117
OpusCodecBinding(const OpusCodecBinding &)=delete
@ ERROR_INVALID_STATE
@ ERROR_AUDIO
Definition error_codes.h:62
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
@ ERROR_AUDIO_IN_USE
Definition error_codes.h:63
asciichat_error_t audio_processor_create(const audio_processor_config_t *c, audio_processor_t **out)
Definition processor.c:71
asciichat_error_t audio_processor_process(audio_processor_t *p, const float *input, float *output, size_t count)
Definition processor.c:125
void audio_processor_destroy(audio_processor_t *p)
Definition processor.c:229
audio_processor_config_t audio_processor_default_config(int sample_rate)
Definition processor.c:29
struct audio_processor audio_processor_t
Definition processor.h:13
int client_opus_decoder_init(int sample_rate, int channels)
void client_opus_decoder_cleanup(void)
void client_opus_encoder_cleanup(void)
int client_opus_encode(const int16_t *pcm_data, int frame_size, uint8_t *opus_data, int max_opus_bytes)
int client_opus_decode(const uint8_t *opus_data, int opus_bytes, int16_t *pcm_data, int frame_size, int decode_fec)
int client_opus_encoder_init(int sample_rate, int channels, int bitrate)
int channels
Interleaved PCM channels; zero retains the mono API default.
Definition processor.h:25
audio_processor_flags_t flags
Definition processor.h:27
For audio processor, this is the live record that ties config, spectral_left, spectral_right,...
Definition processor.c:14