Skip to content

Commit d30113e

Browse files
committed
feat(voice): enhance audio processing with PCM file playback and CRC optimization
1 parent 56b5a89 commit d30113e

6 files changed

Lines changed: 94 additions & 19 deletions

File tree

src/Discord/Voice/Dave/MediaCryptoService.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,6 @@ public function encrypt(string $frame, ?int $ssrc): string
7272
return '';
7373
}
7474

75-
$this->logger->debug('Encrypted outgoing DAVE frame.', [
76-
'protocol_version' => $protocolVersion,
77-
'frame_length' => strlen($encrypted),
78-
]);
79-
8075
return $encrypted;
8176
}
8277

src/Discord/Voice/Helpers/Buffer.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,13 @@ public function close(): void
217217
}
218218

219219
$this->closed = true;
220+
221+
// Reject any reads still waiting — the stream is gone and no more data will arrive.
222+
foreach ($this->reads as [$deferred]) {
223+
$deferred->reject(new \RuntimeException('Buffer closed'));
224+
}
225+
$this->reads = [];
226+
220227
$this->emit('close');
221228
}
222229
}

src/Discord/Voice/OggPage.php

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -137,27 +137,59 @@ public static function fromBuffer(Buffer $buffer, ?int $timeout = -1): PromiseIn
137137
}
138138

139139
/**
140-
* Compute the Ogg-specific CRC-32 checksum.
140+
* Precomputed 256-entry CRC lookup table (Ogg polynomial 0x04C11DB7).
141+
* Populated once on first use via {@see buildCrcTable()}.
142+
*
143+
* @var int[]|null
144+
*/
145+
private static ?array $crcTable = null;
146+
147+
/**
148+
* Build the 256-entry CRC lookup table for polynomial 0x04C11DB7.
149+
*
150+
* @return int[]
151+
*/
152+
private static function buildCrcTable(): array
153+
{
154+
$table = [];
155+
for ($i = 0; $i < 256; $i++) {
156+
$crc = $i << 24;
157+
for ($j = 0; $j < 8; $j++) {
158+
$crc = ($crc & 0x80000000)
159+
? (($crc << 1) ^ 0x04C11DB7) & 0xFFFFFFFF
160+
: ($crc << 1) & 0xFFFFFFFF;
161+
}
162+
$table[$i] = $crc;
163+
}
164+
165+
return $table;
166+
}
167+
168+
/**
169+
* Compute the Ogg-specific CRC-32 checksum using a precomputed lookup table.
141170
*
142171
* Uses polynomial 0x04C11DB7 with an initial value of 0 and no final XOR,
143172
* which differs from the standard CRC-32 used by PHP's crc32().
144173
*
174+
* The lookup table reduces the inner 8-iteration bit loop to a single
175+
* array access per byte, significantly reducing CPU time on the event loop.
176+
*
145177
* @param string $data Raw page bytes with the 4-byte checksum field zeroed out.
146178
*
147179
* @return int The computed CRC-32 value.
148180
*/
149181
private static function oggCrc32(string $data): int
150182
{
183+
if (self::$crcTable === null) {
184+
self::$crcTable = self::buildCrcTable();
185+
}
186+
151187
$crc = 0;
152-
for ($i = 0; $i < strlen($data); $i++) {
153-
$crc ^= (ord($data[$i]) << 24);
154-
for ($j = 0; $j < 8; $j++) {
155-
if ($crc & 0x80000000) {
156-
$crc = (($crc << 1) ^ 0x04C11DB7) & 0xFFFFFFFF;
157-
} else {
158-
$crc = ($crc << 1) & 0xFFFFFFFF;
159-
}
160-
}
188+
$len = strlen($data);
189+
$table = self::$crcTable;
190+
191+
for ($i = 0; $i < $len; $i++) {
192+
$crc = ($table[(($crc >> 24) ^ ord($data[$i])) & 0xFF] ^ ($crc << 8)) & 0xFFFFFFFF;
161193
}
162194

163195
return $crc;

src/Discord/Voice/OggStream.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public function getPacket(): PromiseInterface
122122
private function parsePackets(): PromiseInterface
123123
{
124124
return new Promise(function ($resolve, $reject) {
125-
OggPage::fromBuffer($this->buffer, timeout: 0)->then(function ($page) use ($resolve) {
125+
OggPage::fromBuffer($this->buffer, timeout: -1)->then(function ($page) use ($resolve) {
126126
$packets = [];
127127
$partial = $this->leftover;
128128
foreach ($page->iterPackets() as [$data, $complete]) {
@@ -136,7 +136,8 @@ private function parsePackets(): PromiseInterface
136136

137137
$resolve($packets);
138138
}, function (\Exception $e) use ($resolve, $reject) {
139-
if ($e instanceof BufferTimedOutException) {
139+
if ($e instanceof BufferTimedOutException || str_contains($e->getMessage(), 'Buffer closed')) {
140+
// Buffer is exhausted — signal EOF to the caller.
140141
$resolve(null);
141142
} else {
142143
$reject($e);

src/Discord/Voice/Processes/Ffmpeg.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public static function encode(
7373
?array $preArgs = null
7474
): Process {
7575
$flags = [
76-
'-protocol_whitelist', 'http,https,tcp,tls,crypto,pipe',
76+
'-protocol_whitelist', 'file,http,https,tcp,tls,crypto,pipe',
7777
'-i', escapeshellarg($filename ?? 'pipe:0'),
7878
'-map_metadata', '-1',
7979
'-f', 'opus',

src/Discord/Voice/VoiceClient.php

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,10 @@ public static function checkForExecutable(string $executable): ?string
471471
/**
472472
* Plays a file/url on the voice stream.
473473
*
474+
* FFmpeg is used to decode the file, so any format ffmpeg supports is accepted
475+
* (e.g. WAV, OGG Opus, MP3, FLAC). Raw PCM files (.pcm) are NOT supported here
476+
* because ffmpeg cannot auto-detect the format — use {@see playPcmFile()} instead.
477+
*
474478
* @param string $file The file/url to play.
475479
* @param int $channels Deprecated, Discord only supports 2 channels.
476480
*
@@ -595,7 +599,42 @@ public function playRawStream($stream, int $channels = 2, int $audioRate = 48000
595599
}
596600

597601
/**
598-
* Plays an Ogg Opus stream.
602+
* Plays a raw PCM file on the voice stream.
603+
*
604+
* This is a convenience wrapper around {@see playRawStream()} for files saved
605+
* as raw signed 16-bit little-endian PCM (e.g. recordings produced by
606+
* {@see record()} with {@see RecordingFormat::PCM}).
607+
*
608+
* @param string $path Absolute or relative path to the raw PCM file.
609+
* @param int $channels Number of audio channels (default: 2 = stereo).
610+
* @param int $audioRate Sample rate in Hz (default: 48000).
611+
*
612+
* @throws FileNotFoundException if the file does not exist.
613+
* @throws \RuntimeException if the file cannot be opened or audio is already playing.
614+
*
615+
* @return PromiseInterface
616+
*/
617+
public function playPcmFile(string $path, int $channels = 2, int $audioRate = 48000): PromiseInterface
618+
{
619+
$deferred = new Deferred();
620+
621+
if (! file_exists($path)) {
622+
$deferred->reject(new FileNotFoundException("Could not find the file \"{$path}\"."));
623+
624+
return $deferred->promise();
625+
}
626+
627+
$handle = fopen($path, 'rb');
628+
if ($handle === false) {
629+
$deferred->reject(new \RuntimeException("Could not open file for reading: \"{$path}\"."));
630+
631+
return $deferred->promise();
632+
}
633+
634+
return $this->playRawStream($handle, $channels, $audioRate);
635+
}
636+
637+
/**
599638
*
600639
* @param resource|Process|Stream $stream The Ogg Opus stream to be sent.
601640
*
@@ -644,6 +683,7 @@ public function playOggStream($stream): PromiseInterface
644683

645684
$this->buffer = new RealBuffer();
646685
$stream->on('data', fn ($d) => $this->buffer->write($d));
686+
$stream->on('end', fn () => $this->buffer->end());
647687

648688
/** @var OggStream */
649689
$ogg = null;

0 commit comments

Comments
 (0)