Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add synchronization to start of audio recordings #1984

Merged
merged 34 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f995624
Basic idea
OmLanke Mar 22, 2023
952609b
Merge branch 'Pycord-Development:master' into voice-recording
OmLanke Mar 23, 2023
93df7a3
trying to use internal clock to time packets
OmLanke Mar 23, 2023
4d62fd3
Merge branch 'Pycord-Development:master' into voice-recording
OmLanke Mar 24, 2023
ee53416
Merge branch 'voice-recording' of https://github.com/Om1609/pycord in…
OmLanke Mar 24, 2023
7eabc87
final solution and cleanup
OmLanke Mar 24, 2023
66890d8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 24, 2023
0484a0c
Update CHANGELOG.md
OmLanke Mar 24, 2023
27b2d3e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 24, 2023
653f574
Example
OmLanke Mar 24, 2023
a30c89e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 24, 2023
81b0112
Merge branch 'master' into voice-recording
OmLanke Mar 24, 2023
a06a08b
Merge branch 'master' into voice-recording
OmLanke Mar 25, 2023
c70806e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 25, 2023
74e1040
Update CHANGELOG.md
OmLanke Mar 25, 2023
36b2feb
optionally use internal clock
OmLanke Mar 29, 2023
13464ee
Update discord/voice_client.py
OmLanke Apr 2, 2023
a6f2a48
Merge branch 'master' into voice-recording
OmLanke Apr 2, 2023
b5924eb
Merge branch 'voice-recording' of https://github.com/Om1609/pycord in…
OmLanke Apr 2, 2023
36690d7
Merge branch 'Pycord-Development:master' into voice-recording
OmLanke Apr 3, 2023
9c57b8c
Merge branch 'voice-recording' of https://github.com/Om1609/pycord in…
OmLanke Apr 3, 2023
90cf800
Revert "optionally use internal clock"
OmLanke Apr 4, 2023
afdd1ea
implement receive time as fallback
OmLanke Apr 4, 2023
6185ff6
implement receive time as fallback
OmLanke Apr 4, 2023
0257254
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 4, 2023
f55eeb3
remove fmt comments
OmLanke Apr 4, 2023
f49d033
Merge branch 'master' into voice-recording
plun1331 Apr 8, 2023
d67a1c9
Merge branch 'master' into voice-recording
OmLanke Apr 17, 2023
c7c80d0
Merge branch 'master' into voice-recording
Lulalaby Apr 18, 2023
901285c
Add pydub version
OmLanke Apr 19, 2023
b8d6f4b
Merge branch 'master' into voice-recording
OmLanke Apr 27, 2023
72a312d
Merge branch 'master' into voice-recording
OmLanke Apr 29, 2023
65e7bcf
change condition
OmLanke Apr 29, 2023
ecd3177
Merge branch 'master' into voice-recording
plun1331 May 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ These changes are available on the `master` branch, but have not yet been releas
([#1983](https://github.com/Pycord-Development/pycord/pull/1983))
- Added new `application_auto_moderation_rule_create_badge` to `ApplicationFlags`.
([#1992](https://github.com/Pycord-Development/pycord/pull/1992))
- Added `sync_start` argument to `VoiceClient.start_recording()`. This adds silence to
the start of audio recordings.
([#1984](https://github.com/Pycord-Development/pycord/pull/1984))

### Changed

Expand Down
1 change: 1 addition & 0 deletions discord/sinks/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def __init__(self, data, client):
self.decoded_data = None

self.user_id = None
self.receive_time = time.perf_counter()


class AudioData:
Expand Down
50 changes: 36 additions & 14 deletions discord/voice_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,7 @@ def unpack_audio(self, data):

self.decoder.decode(data)

def start_recording(self, sink, callback, *args):
def start_recording(self, sink, callback, *args, sync_start: bool = False):
"""The bot will begin recording audio from the current voice channel it is in.
This function uses a thread so the current code line will not be stopped.
Must be in a voice channel to use.
Expand All @@ -716,6 +716,9 @@ def start_recording(self, sink, callback, *args):
A function which is called after the bot has stopped recording.
*args:
Args which will be passed to the callback function.
sync_start: :class:`bool`
If True, the recordings of subsequent users will start with silence.
This is useful for recording audio just as it was heard.

Raises
------
Expand All @@ -738,6 +741,7 @@ def start_recording(self, sink, callback, *args):
self.decoder = opus.DecodeManager(self)
self.decoder.start()
self.recording = True
self.sync_start = sync_start
self.sink = sink
sink.init(self)

Expand Down Expand Up @@ -796,8 +800,9 @@ def recv_audio(self, sink, callback, *args):
# it by user, handles pcm files and
# silence that should be added.

self.user_timestamps = {}
self.user_timestamps: dict[int, tuple[int, float]] = {}
self.starting_time = time.perf_counter()
self.first_packet_timestamp: float
while self.recording:
ready, _, err = select.select([self.socket], [], [self.socket], 0.01)
if not ready:
Expand All @@ -815,27 +820,44 @@ def recv_audio(self, sink, callback, *args):

self.stopping_time = time.perf_counter()
self.sink.cleanup()
callback = asyncio.run_coroutine_threadsafe(
callback(self.sink, *args), self.loop
)
callback = asyncio.run_coroutine_threadsafe(callback(sink, *args), self.loop)
result = callback.result()

if result is not None:
print(result)

def recv_decoded_audio(self, data):
if data.ssrc not in self.user_timestamps:
self.user_timestamps.update({data.ssrc: data.timestamp})
# Add silence when they were not being recorded.
silence = 0
else:
silence = data.timestamp - self.user_timestamps[data.ssrc] - 960
self.user_timestamps[data.ssrc] = data.timestamp
def recv_decoded_audio(self, data: RawData):
# Add silence when they were not being recorded.
if data.ssrc not in self.user_timestamps: # First packet from user
if (
not self.user_timestamps or not self.sync_start
): # First packet from anyone
self.first_packet_timestamp = data.receive_time
silence = 0

else: # Previously received a packet from someone else
silence = (
(data.receive_time - self.first_packet_timestamp) * 48000
) - 960

else: # Previously received a packet from user
dRT = (
data.receive_time - self.user_timestamps[data.ssrc][1]
) * 48000 # delta receive time
dT = data.timestamp - self.user_timestamps[data.ssrc][0] # delta timestamp
diff = abs(100 - dT * 100 / dRT)
if diff > 100 or dT != 960: # If the difference is more than 100%
silence = dRT - 960
else:
silence = dT - 960

self.user_timestamps.update({data.ssrc: (data.timestamp, data.receive_time)})

data.decoded_data = (
struct.pack("<h", 0) * silence * opus._OpusStruct.CHANNELS
struct.pack("<h", 0) * max(0, int(silence)) * opus._OpusStruct.CHANNELS
+ data.decoded_data
)

while data.ssrc not in self.ws.ssrc_map:
time.sleep(0.05)
self.sink.write(data.decoded_data, self.ws.ssrc_map[data.ssrc]["user_id"])
Expand Down
115 changes: 115 additions & 0 deletions examples/audio_recording_merged.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import io

import pydub # pip install pydub
OmLanke marked this conversation as resolved.
Show resolved Hide resolved

import discord
from discord.sinks import MP3Sink

bot = discord.Bot()
connections: dict[int, discord.VoiceClient] = {}


@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")


async def finished_callback(sink: MP3Sink, channel: discord.TextChannel):
mention_strs = []
audio_segs: list[pydub.AudioSegment] = []
files: list[discord.File] = []

longest = pydub.AudioSegment.empty()

for user_id, audio in sink.audio_data.items():
mention_strs.append(f"<@{user_id}>")

seg = pydub.AudioSegment.from_file(audio.file, format="mp3")

# Determine the longest audio segment
if len(seg) > len(longest):
audio_segs.append(longest)
longest = seg
else:
audio_segs.append(seg)

audio.file.seek(0)
files.append(discord.File(audio.file, filename=f"{user_id}.mp3"))

for seg in audio_segs:
longest = longest.overlay(seg)

with io.BytesIO() as f:
longest.export(f, format="mp3")
await channel.send(
f"Finished! Recorded audio for {', '.join(mention_strs)}.",
files=files + [discord.File(f, filename="recording.mp3")],
)


@bot.command()
async def join(ctx: discord.ApplicationContext):
"""Join the voice channel!"""
voice = ctx.author.voice

if not voice:
return await ctx.respond("You're not in a vc right now")

vc = await voice.channel.connect()
connections.update({ctx.guild.id: vc})

await ctx.respond("Joined!")


@bot.command()
async def start(ctx: discord.ApplicationContext):
"""Record the voice channel!"""
voice = ctx.author.voice

if not voice:
return await ctx.respond("You're not in a vc right now")

vc = connections.get(ctx.guild.id)

if not vc:
return await ctx.respond(
"I'm not in a vc right now. Use `/join` to make me join!"
)

vc.start_recording(
MP3Sink(),
finished_callback,
ctx.channel,
sync_start=True,
)

await ctx.respond("The recording has started!")


@bot.command()
async def stop(ctx: discord.ApplicationContext):
"""Stop the recording"""
vc = connections.get(ctx.guild.id)

if not vc:
return await ctx.respond("There's no recording going on right now")

vc.stop_recording()

await ctx.respond("The recording has stopped!")


@bot.command()
async def leave(ctx: discord.ApplicationContext):
"""Leave the voice channel!"""
vc = connections.get(ctx.guild.id)

if not vc:
return await ctx.respond("I'm not in a vc right now")

await vc.disconnect()

await ctx.respond("Left!")


bot.run("TOKEN")