Repository navigation
Expand file tree
/
Copy pathOgg.php
More file actions
597 lines (554 loc) · 24.9 KB
/
Copy pathOgg.php
File metadata and controls
597 lines (554 loc) · 24.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
<?php
declare(strict_types=1);
/**
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto 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 Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2025 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto;
use Amp\ByteStream\ReadableIterableStream;
use Amp\ByteStream\ReadableStream;
use Amp\ByteStream\WritableStream;
use Amp\Cancellation;
use Amp\DeferredCancellation;
use Amp\Process\Process;
use AssertionError;
use Closure;
use FFI;
use FFI\CData;
use Throwable;
use Webmozart\Assert\Assert;
use function Amp\async;
use function Amp\ByteStream\getStderr;
use function Amp\ByteStream\pipe;
use function Amp\ByteStream\splitLines;
use function Amp\File\openFile;
use function count;
/**
* Async OGG stream reader and writer.
*
* @author Charles-Édouard Coste <contact@ccoste.fr>
* @author Daniil Gentili <daniil@daniil.it>
*/
final class Ogg
{
private const OPUS_SET_APPLICATION_REQUEST = 4000;
private const OPUS_GET_APPLICATION_REQUEST = 4001;
private const OPUS_SET_BITRATE_REQUEST = 4002;
private const OPUS_GET_BITRATE_REQUEST = 4003;
private const OPUS_SET_MAX_BANDWIDTH_REQUEST = 4004;
private const OPUS_GET_MAX_BANDWIDTH_REQUEST = 4005;
private const OPUS_SET_VBR_REQUEST = 4006;
private const OPUS_GET_VBR_REQUEST = 4007;
private const OPUS_SET_BANDWIDTH_REQUEST = 4008;
private const OPUS_GET_BANDWIDTH_REQUEST = 4009;
private const OPUS_SET_COMPLEXITY_REQUEST = 4010;
private const OPUS_GET_COMPLEXITY_REQUEST = 4011;
private const OPUS_SET_INBAND_FEC_REQUEST = 4012;
private const OPUS_GET_INBAND_FEC_REQUEST = 4013;
private const OPUS_SET_PACKET_LOSS_PERC_REQUEST = 4014;
private const OPUS_GET_PACKET_LOSS_PERC_REQUEST = 4015;
private const OPUS_SET_DTX_REQUEST = 4016;
private const OPUS_GET_DTX_REQUEST = 4017;
private const OPUS_SET_VBR_CONSTRAINT_REQUEST = 4020;
private const OPUS_GET_VBR_CONSTRAINT_REQUEST = 4021;
private const OPUS_SET_FORCE_CHANNELS_REQUEST = 4022;
private const OPUS_GET_FORCE_CHANNELS_REQUEST = 4023;
private const OPUS_SET_SIGNAL_REQUEST = 4024;
private const OPUS_GET_SIGNAL_REQUEST = 4025;
private const OPUS_GET_LOOKAHEAD_REQUEST = 4027;
private const OPUS_GET_SAMPLE_RATE_REQUEST = 4029;
private const OPUS_GET_FINAL_RANGE_REQUEST = 4031;
private const OPUS_GET_PITCH_REQUEST = 4033;
private const OPUS_SET_GAIN_REQUEST = 4034;
private const OPUS_GET_GAIN_REQUEST = 4045;
private const OPUS_SET_LSB_DEPTH_REQUEST = 4036;
private const OPUS_GET_LSB_DEPTH_REQUEST = 4037;
private const OPUS_GET_LAST_PACKET_DURATION_REQUEST = 4039;
private const OPUS_SET_EXPERT_FRAME_DURATION_REQUEST = 4040;
private const OPUS_GET_EXPERT_FRAME_DURATION_REQUEST = 4041;
private const OPUS_SET_PREDICTION_DISABLED_REQUEST = 4042;
private const OPUS_GET_PREDICTION_DISABLED_REQUEST = 4043;
private const OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST = 4046;
private const OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST = 4047;
private const OPUS_GET_IN_DTX_REQUEST = 4049;
/* Values for the various encoder CTLs */
private const OPUS_AUTO = -1000 /**<Auto/default setting @hideinitializer*/;
private const OPUS_BITRATE_MAX = -1 /**<Maximum bitrate @hideinitializer*/;
/** Best for most VoIP/videoconference applications where listening quality and intelligibility matter most.
* @hideinitializer */
private const OPUS_APPLICATION_VOIP = 2048;
/** Best for broadcast/high-fidelity application where the decoded audio should be as close as possible to the input.
* @hideinitializer */
private const OPUS_APPLICATION_AUDIO = 2049;
/** Only use when lowest-achievable latency is what matters most. Voice-optimized modes cannot be used.
* @hideinitializer */
private const OPUS_APPLICATION_RESTRICTED_LOWDELAY = 2051;
private const OPUS_SIGNAL_VOICE = 3001 /**< Signal being encoded is voice */;
private const OPUS_SIGNAL_MUSIC = 3002 /**< Signal being encoded is music */;
private const OPUS_BANDWIDTH_NARROWBAND = 1101 /**< 4 kHz bandpass @hideinitializer*/;
private const OPUS_BANDWIDTH_MEDIUMBAND = 1102 /**< 6 kHz bandpass @hideinitializer*/;
private const OPUS_BANDWIDTH_WIDEBAND = 1103 /**< 8 kHz bandpass @hideinitializer*/;
private const OPUS_BANDWIDTH_SUPERWIDEBAND = 1104 /**<12 kHz bandpass @hideinitializer*/;
private const OPUS_BANDWIDTH_FULLBAND = 1105 /**<20 kHz bandpass @hideinitializer*/;
private const OPUS_FRAMESIZE_ARG = 5000 /**< Select frame size from the argument (default) */;
private const OPUS_FRAMESIZE_2_5_MS = 5001 /**< Use 2.5 ms frames */;
private const OPUS_FRAMESIZE_5_MS = 5002 /**< Use 5 ms frames */;
private const OPUS_FRAMESIZE_10_MS = 5003 /**< Use 10 ms frames */;
private const OPUS_FRAMESIZE_20_MS = 5004 /**< Use 20 ms frames */;
private const OPUS_FRAMESIZE_40_MS = 5005 /**< Use 40 ms frames */;
private const OPUS_FRAMESIZE_60_MS = 5006 /**< Use 60 ms frames */;
private const OPUS_FRAMESIZE_80_MS = 5007 /**< Use 80 ms frames */;
private const OPUS_FRAMESIZE_100_MS = 5008 /**< Use 100 ms frames */;
private const OPUS_FRAMESIZE_120_MS = 5009 /**< Use 120 ms frames */;
private const CAPTURE_PATTERN = "OggS";
public const CONTINUATION = 1;
public const BOS = 2;
public const EOS = 4;
public const STATE_READ_HEADER = 0;
public const STATE_READ_COMMENT = 1;
public const STATE_STREAMING = 3;
public const STATE_END = 4;
/**
* OGG Stream count.
*/
private int $streamCount;
/**
* Total number of bytes read from the underlying source, i.e. the byte offset of the read
* cursor. Starts at the resume offset so that {@see self::$pageOffset} is an absolute file offset.
*/
private int $bytesRead = 0;
/**
* Byte offset of the OGG page currently being read.
*
* This is a page boundary, so re-opening the file here with the `$startOffset` constructor
* argument resumes reading exactly this page, which is what makes playback byte-offset
* resumable across a serialize/unserialize cycle.
*/
public int $pageOffset = 0;
/** Whether reading started partway through the file, so the OPUS headers are not present. */
private bool $resuming = false;
/**
* Pack format.
*/
private string $packFormat;
/**
* Opus packet iterator.
*
* @var iterable<string>
*/
public readonly iterable $opusPackets;
public readonly string $vendorString;
/** @var list<string> */
public readonly array $comments;
/**
* @var (Closure(int, ?Cancellation): ?string) $stream The stream
*/
private readonly Closure $stream;
/**
* Constructor.
*/
public function __construct(LocalFile|RemoteUrl|ReadableStream $stream, ?Cancellation $cancellation = null, int $startOffset = 0)
{
// Count every byte read so that the offset of each page is known and playback can resume
// from a byte offset. A non-zero start offset means we opened partway through the file,
// past the OPUS headers, so streaming starts immediately (see read()).
$inner = Tools::openBuffered($stream, $cancellation, $startOffset);
$this->bytesRead = $startOffset;
$this->resuming = $startOffset > 0;
if ($this->resuming) {
// The headers were already parsed on the first open; a MadelineProto/converted OGG
// OPUS file is a single logical stream, which is all the state opusStateMachine needs.
$this->streamCount = 1;
}
$this->stream = function (int $len) use ($inner): ?string {
$data = $inner($len);
if ($data !== null) {
$this->bytesRead += \strlen($data);
}
return $data;
};
$pack_format = [
'stream_structure_version' => 'C',
'header_type_flag' => 'C',
'granule_position' => 'P',
'bitstream_serial_number' => 'V',
'page_sequence_number' => 'V',
'CRC_checksum' => 'V',
'number_page_segments' => 'C',
];
$this->packFormat = implode(
'/',
array_map(
static fn (string $v, string $k): string => $v.$k,
$pack_format,
array_keys($pack_format),
),
);
$it = $this->read();
$it->current();
if (!$this->resuming && !isset($this->comments)) {
throw new Exception('Not an OGG OPUS file!');
}
$this->opusPackets = $it;
}
/**
* Get the duration of an OPUS packet, in 48kHz samples (0 if the packet is malformed).
*
* The duration is read from the TOC byte (and, for packets with an arbitrary number of frames,
* from the frame count byte) of the packet, see [RFC 6716 §3.1](https://www.rfc-editor.org/rfc/rfc6716#section-3.1).
*
* @psalm-pure
*/
public static function packetDuration(string $packet): int
{
if ($packet === '') {
return 0;
}
$toc = \ord($packet[0]);
$config = $toc >> 3;
// SILK-only: 10, 20, 40, 60ms; hybrid: 10, 20ms; CELT-only: 2.5, 5, 10, 20ms.
$frameSamples = match (true) {
$config < 12 => [480, 960, 1920, 2880][$config % 4],
$config < 16 => [480, 960][$config % 2],
default => [120, 240, 480, 960][$config % 4],
};
$frames = match ($toc & 3) {
0 => 1,
1, 2 => 2,
default => isset($packet[1]) ? \ord($packet[1]) & 0x3F : 0,
};
$duration = $frames * $frameSamples;
// A packet can't be longer than 120ms.
return $duration <= 5760 ? $duration : 0;
}
/**
* Get the OPUS packets of a complete OGG packet.
*
* Every OGG packet holds exactly one OPUS packet, which is passed through as-is.
*/
private function opusStateMachine(string $content): \Generator
{
if ($this->streamCount !== 1) {
throw new AssertionError("Multistream (surround) OGG OPUS files are not supported, please convert them to mono or stereo!");
}
if (self::packetDuration($content) !== 0) {
yield $content;
}
}
/**
* Validate that the specified file, URL or stream is a valid VoIP OGG OPUS file.
*/
public function validate(LocalFile|RemoteUrl|ReadableStream $file, ?Cancellation $cancellation = null): void
{
foreach ((new self($file, $cancellation))->opusPackets as $_) {
}
}
/**
* Read frames.
*
* @return \Generator<string>
*/
private function read(): \Generator
{
// When resuming from a byte offset we jump straight to the streaming state: the offset is a
// page boundary past the OpusHead/comment headers, which were parsed on the first open.
$state = $this->resuming ? self::STATE_STREAMING : self::STATE_READ_HEADER;
$content = '';
$granule = 0;
$ignoredStreams = [];
while (true) {
// Record the byte offset of this page before reading it, so a caller can resume here.
$this->pageOffset = $this->bytesRead;
$capture = ($this->stream)(4);
if ($capture !== self::CAPTURE_PATTERN) {
if ($capture === null) {
return;
}
throw new Exception('Bad capture pattern: '.bin2hex($capture));
}
$headers = unpack(
$this->packFormat,
($this->stream)(23)
);
$ignore = \in_array($headers['bitstream_serial_number'], $ignoredStreams, true);
if ($headers['stream_structure_version'] != 0x00) {
throw new Exception("Bad stream version");
}
$granule_diff = $headers['granule_position'] - $granule;
$granule = $headers['granule_position'];
$continuation = (bool) ($headers['header_type_flag'] & 0x01);
$firstPage = (bool) ($headers['header_type_flag'] & self::BOS);
$lastPage = (bool) ($headers['header_type_flag'] & self::EOS);
$segments = unpack(
'C*',
($this->stream)($headers['number_page_segments']),
);
$sizeAccumulated = 0;
foreach ($segments as $segment_size) {
$sizeAccumulated += $segment_size;
if ($segment_size < 255) {
$piece = ($this->stream)($sizeAccumulated);
$sizeAccumulated = 0;
if ($ignore) {
continue;
}
$content .= $piece;
if ($state === self::STATE_STREAMING) {
yield from $this->opusStateMachine($content);
} elseif ($state === self::STATE_READ_HEADER) {
Assert::true($firstPage);
$head = substr($content, 0, 8);
if ($head !== 'OpusHead') {
$ignoredStreams[]= $headers['bitstream_serial_number'];
$content = '';
$ignore = true;
continue;
}
$opus_head = unpack('Cversion/Cchannel_count/vpre_skip/Vsample_rate/voutput_gain/Cchannel_mapping_family/', substr($content, 8));
if ($opus_head['channel_mapping_family']) {
$opus_head['channel_mapping'] = unpack('Cstream_count/Ccoupled_count/C*channel_mapping', substr($content, 19));
} else {
$opus_head['channel_mapping'] = [
'stream_count' => 1,
'coupled_count' => $opus_head['channel_count'] - 1,
'channel_mapping' => [0],
];
if ($opus_head['channel_count'] === 2) {
$opus_head['channel_mapping']['channel_mapping'][] = 1;
}
}
$this->streamCount = $opus_head['channel_mapping']['stream_count'];
if ($opus_head['sample_rate'] !== 48000) {
throw new AssertionError("The sample rate must be 48khz, got {$opus_head['sample_rate']}");
}
$state = self::STATE_READ_COMMENT;
} elseif ($state === self::STATE_READ_COMMENT) {
$vendor_string_length = unpack('V', substr($content, 8, 4))[1];
$this->vendorString = substr($content, 12, $vendor_string_length);
$comment_count = unpack('V', substr($content, 12+$vendor_string_length, 4))[1];
$offset = 16+$vendor_string_length;
$comments = [];
for ($x = 0; $x < $comment_count; $x++) {
$length = unpack('V', substr($content, $offset, 4))[1];
$comments []= substr($content, $offset += 4, $length);
$offset += $length;
}
$this->comments = $comments;
$state = self::STATE_STREAMING;
}
$content = '';
}
}
}
}
/**
* Converts a file, URL, or stream of any format (including video) into an OGG audio stream suitable for consumption by MadelineProto's VoIP implementation.
*
* @param LocalFile|RemoteUrl|ReadableStream $in The input file, URL or stream.
* @param LocalFile|WritableStream $oggOut The output file or stream.
*/
public static function convert(
LocalFile|RemoteUrl|ReadableStream $in,
LocalFile|WritableStream $oggOut,
?Cancellation $cancellation = null
): void {
$inFile = match (true) {
$in instanceof LocalFile => $in->file,
$in instanceof RemoteUrl => $in->url,
$in instanceof ReadableStream => '/dev/stdin',
};
$proc = Process::start(['ffmpeg', '-hide_banner', '-loglevel', 'warning', '-i', $inFile, '-map', '0:a', '-ar', '48000', '-f', 'wav', '-y', '/dev/stdout'], cancellation: $cancellation);
if ($in instanceof ReadableStream) {
async(pipe(...), $in, $proc->getStdin(), $cancellation)
->ignore()
->finally($proc->getStdin()->close(...));
}
// Forwarded to our stderr, its last lines kept to explain a failure.
$errors = [];
$stderr = async(static function () use ($proc, &$errors, $cancellation): void {
$out = getStderr();
foreach (splitLines($proc->getStderr(), $cancellation) as $line) {
$out->write("$line\n");
$errors[] = $line;
if (\count($errors) > self::FFMPEG_ERROR_LINES) {
array_shift($errors);
}
}
});
$stderr->ignore();
$stdout = $proc->getStdout();
$first = $stdout->read($cancellation);
if ($first === null) {
// ffmpeg couldn't read the input (not media, unreachable...): its exit code and error, not the WAV
// parser's complaint about an empty file.
$code = $proc->join($cancellation);
$stderr->await($cancellation);
$what = $in instanceof ReadableStream ? 'the stream' : $inFile;
throw new AssertionError("ffmpeg could not convert $what (exit code $code): ".trim(implode("\n", $errors)));
}
self::convertWav(new ReadableIterableStream((static function () use ($first, $stdout, $cancellation): \Generator {
yield $first;
while (($chunk = $stdout->read($cancellation)) !== null) {
yield $chunk;
}
})()), $oggOut, $cancellation);
}
/** How many of the last lines of ffmpeg's error output to report when it fails. */
private const FFMPEG_ERROR_LINES = 10;
/**
* Validate that the specified OGG OPUS file can be played directly by MadelineProto, without doing any conversion.
*
* @throws \Throwable If validation fails.
*/
public static function validateOgg(LocalFile|RemoteUrl|ReadableStream $f): void
{
$ok = false;
$e = null;
try {
try {
$cancel = new DeferredCancellation;
$ogg = new self($f, $cancel->getCancellation());
$ok = \in_array('MADELINE_ENCODER_V=1', $ogg->comments, true);
} finally {
$cancel->cancel();
}
} catch (\Throwable $e) {
}
if (!$ok) {
throw new AssertionError("The passed file was not generated by MadelineProto or @libtgvoipbot, please pre-convert it using @libtgvoipbot or install FFI and ffmpeg to perform realtime conversion!", 0, $e);
}
}
private const CDEF = '
typedef struct OpusEncoder OpusEncoder;
OpusEncoder *opus_encoder_create(
int32_t Fs,
int channels,
int application,
int *error
);
int opus_encoder_ctl(OpusEncoder *st, int request, int arg);
int32_t opus_encode(
OpusEncoder *st,
const char *pcm,
int frame_size,
const char *data,
int32_t max_data_bytes
);
void opus_encoder_destroy(OpusEncoder *st);
const char *opus_strerror(int error);
const char *opus_get_version_string(void);
';
private static ?FFI $FFI = null;
/**
* libopus' encoder API, through FFI.
*
* @internal
*
* @throws Throwable If FFI or libopus is not available.
*/
public static function libopus(): FFI
{
if (self::$FFI !== null) {
return self::$FFI;
}
foreach (['libopus.so', 'libopus.so.0'] as $k => $lib) {
try {
return self::$FFI = FFI::cdef(self::CDEF, $lib);
} catch (Throwable $e) {
if ($k) {
throw $e;
}
}
}
throw new AssertionError('Unreachable');
}
/**
* Converts a file, URL, or stream in WAV format @ 48khz into an OGG audio stream suitable for consumption by MadelineProto's VoIP implementation.
*
* @param LocalFile|RemoteUrl|ReadableStream $wavIn The input file, URL or stream.
* @param LocalFile|WritableStream $oggOut The output file or stream.
*/
public static function convertWav(
LocalFile|RemoteUrl|ReadableStream $wavIn,
LocalFile|WritableStream $oggOut,
?Cancellation $cancellation = null
): void {
$opus = self::libopus();
$checkErr = static function (int|CData $err) use ($opus): void {
if ($err instanceof CData) {
$err = $err->cdata;
}
if ($err < 0) {
throw new AssertionError("opus returned: ".$opus->opus_strerror($err));
}
};
$err = $opus->new('int');
$read = Tools::openBuffered($wavIn, $cancellation);
$header = $read(4);
if ($header === null) {
throw new AssertionError("Could not convert the file, make sure ffmpeg and libopus are installed!");
}
Assert::eq($header, 'RIFF', "A .wav file must be provided!");
$totalLength = unpack('V', $read(4))[1];
Assert::eq($read(4), 'WAVE', "A .wav file must be provided!");
do {
$type = $read(4);
$length = unpack('V', $read(4))[1];
if ($type === 'fmt ') {
Assert::eq($length, 16);
$contents = $read($length + ($length % 2));
$header = unpack('vaudioFormat/vchannels/VsampleRate/VbyteRate/vblockAlign/vbitsPerSample', $contents);
Assert::eq($header['audioFormat'], 1, "The wav file must contain PCM audio");
Assert::eq($header['sampleRate'], 48000, "The sample rate of the wav file must be 48khz!");
} elseif ($type === 'data') {
break;
} else {
$read($length);
}
} while (true);
$sampleCount = 0.06 * $header['sampleRate'];
$chunkSize = (int) ($sampleCount * $header['channels'] * ($header['bitsPerSample'] >> 3));
$shift = (int) log($header['channels'] * ($header['bitsPerSample'] >> 3), 2);
$encoder = $opus->opus_encoder_create(48000, $header['channels'], self::OPUS_APPLICATION_AUDIO, FFI::addr($err));
$checkErr($err);
$checkErr($opus->opus_encoder_ctl($encoder, self::OPUS_SET_COMPLEXITY_REQUEST, 10));
$checkErr($opus->opus_encoder_ctl($encoder, self::OPUS_SET_PACKET_LOSS_PERC_REQUEST, 1));
$checkErr($opus->opus_encoder_ctl($encoder, self::OPUS_SET_INBAND_FEC_REQUEST, 1));
$checkErr($opus->opus_encoder_ctl($encoder, self::OPUS_SET_SIGNAL_REQUEST, self::OPUS_SIGNAL_MUSIC));
$checkErr($opus->opus_encoder_ctl($encoder, self::OPUS_SET_BANDWIDTH_REQUEST, self::OPUS_BANDWIDTH_FULLBAND));
$checkErr($opus->opus_encoder_ctl($encoder, self::OPUS_SET_BITRATE_REQUEST, 130*1000));
if ($oggOut instanceof LocalFile) {
$oggOut = openFile($oggOut->file, 'w');
}
$writer = new OggWriter($oggOut);
$writer->writeHeader(
$header['channels'],
$header['sampleRate'],
$opus->opus_get_version_string()
);
$buf = $opus->cast($opus->type('char*'), FFI::addr($opus->new('char[1024]')));
do {
$chunkOrig = $read($chunkSize) ?? '';
$chunk = str_pad($chunkOrig, $chunkSize, "\0");
$granuleDiff = \strlen($chunk) >> $shift;
$len = $opus->opus_encode($encoder, $chunk, $granuleDiff, $buf, 1024);
$checkErr($len);
$writer->writeChunk(
FFI::string($buf, $len),
$granuleDiff,
\strlen($chunk) !== \strlen($chunkOrig)
);
} while (\strlen($chunk) === \strlen($chunkOrig));
$opus->opus_encoder_destroy($encoder);
unset($buf, $encoder, $opus);
}
}