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

[Will be a separate addon] Wire Advanced Microphone and Speaker #2722

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
041bc2d
Advanced Microphone and Speaker work
stepa2 Aug 10, 2023
f0fc23d
Implemented EntityEmitSound hook handling
stepa2 Aug 10, 2023
71421a8
Added player voice reproduction support to microphones/speakers
stepa2 Aug 10, 2023
bd9b711
Microphone/speaker fixes
stepa2 Aug 10, 2023
893e065
Merge remote-tracking branch 'remotes/origin-real/master' into pr-mic…
stepa2 Aug 10, 2023
06ad271
Linter pass
stepa2 Aug 10, 2023
161982c
Added duplication support, added tools
stepa2 Aug 10, 2023
4121d71
Adv. Microphone / Speaker fixes
stepa2 Aug 10, 2023
1dd6c9a
Added WireLib.Sound.IsLooped and WireLib.Sound.StripPrefix, added tem…
stepa2 Aug 10, 2023
cef9f6c
Fixed WireLib.Sound.IsLooped
stepa2 Aug 11, 2023
b8459ad
Code style fixes
stepa2 Aug 11, 2023
95bdfe7
Removed debug log in soundlib.lua
stepa2 Aug 12, 2023
93d6543
Added sound.Play listening support
stepa2 Sep 12, 2023
e09681a
Lint fixes
stepa2 Sep 12, 2023
d99ce25
Removed debug logging
stepa2 Oct 2, 2023
319a56b
Merge commit 'b0fa290438124518ae46813cd21bb2dcb168ff2c' into pr-micro…
stepa2 Jan 2, 2024
40f6a7d
Fixed error on microphone dupe pasting
stepa2 Jan 2, 2024
3c84dcc
Fixed `PlayerCanHearPlayersVoice` hook
Jan 2, 2024
b852f6f
Merge branch 'pr-microphone' of https://github.com/conred-gmod/wire i…
Jan 2, 2024
d6f7b21
Linter pass, removed debug logging, small comment improvement
stepa2 Jan 5, 2024
6ca401f
Added some error handling
stepa2 Jan 6, 2024
cf9011e
Fixed Wire_SoundPlay hook not replacing nils with default values
stepa2 Jan 6, 2024
94ded38
Voice transmission optimization
stepa2 Jan 6, 2024
861233d
Added default values for EntityEmitSound hook calling microphone Hand…
stepa2 Jan 6, 2024
c3df358
Added comments for `Mic_SetLive` logic
stepa2 Jan 7, 2024
ef4cb58
Fixed `sound.Play` microphone/speaker support
stepa2 Jan 9, 2024
122bb75
Possibly fixed ReproduceSound error
stepa2 Jan 9, 2024
f40197e
Added fallback position provider for microphone
stepa2 Jan 20, 2024
1b46615
Added nearby players cache to microphone-speaker system, moved some f…
stepa2 Feb 20, 2024
9c5e1b3
Merge remote-tracking branch 'remotes/origin-master/master' into pr-m…
stepa2 Mar 7, 2024
c778e8f
Linter pass
stepa2 Mar 7, 2024
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
2 changes: 2 additions & 0 deletions lua/autorun/wire_load.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ if SERVER then
AddCSLuaFile("wire/flir.lua")
AddCSLuaFile("wire/von.lua")
AddCSLuaFile("wire/sh_modelplug.lua")
AddCSLuaFile("wire/soundlib.lua")

-- client includes
AddCSLuaFile("wire/client/cl_wirelib.lua")
Expand Down Expand Up @@ -85,6 +86,7 @@ include("wire/timedpairs.lua")
include("wire/default_data_decompressor.lua")
include("wire/flir.lua")
include("wire/von.lua")
include("wire/soundlib.lua")

-- server includes
if SERVER then
Expand Down
218 changes: 218 additions & 0 deletions lua/entities/gmod_wire_adv_microphone.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
AddCSLuaFile()

local MIN_VOLUME = 0.02

ENT.Type = "anim"
ENT.Base = "base_wire_entity"
ENT.Author = "stpM64"
ENT.PrintName = "Wire Advanced Microphone"
ENT.Purpose = "Listens to sounds, soundscapes and player voices"
-- Named 'advanced' because 'gmod_wire_microphone' exists in Wire Extras
ENT.WireDebugName = "Advanced Microphone"

-- Note: we listen both serverside and clientside,
-- because some sounds are played clientside only

-- array(Entity(gmod_wire_adv_microphone))
-- Array instead of lookup table because sounds are emitted more often than microphones switched on or off,
Copy link
Contributor

@Vurv78 Vurv78 Aug 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any notable difference in speed between ipairs and pairs in this case with a small amount of items?

Cause I'd really prefer a lookup table over using table.RemoveByValue and table.insert. Even if I know there won't be more than maybe ten microphones placed in a server at once. At worst you could maintain both a lookup table and an array for the best of both worlds.

I know ipairs can jit but that's just on the x86_64 branch. Although you could use a manual for loop, but you aren't doing that.

Copy link
Contributor

@Grocel Grocel Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can only agree on this. And I just don't get it why it such a problem to just change it. It is not like you are going to need to add like extra 1000 lines of code for it.

-- so iteration is more frequent than insertion/removal.
-- Microphone is live when it is active and at least one active speaker connected to it.
_WireLiveMicrophones = _WireLiveMicrophones or {}
local LiveMics = _WireLiveMicrophones

function ENT:SetupDataTables()
self:NetworkVar("Bool", 0, "Active")
self:NetworkVarNotify("Active", self.OnActiveChanged)
end

function ENT:Initialize()
if SERVER then
self:PhysicsInit( SOLID_VPHYSICS )
self:SetMoveType( MOVETYPE_VPHYSICS )
self:SetSolid( SOLID_VPHYSICS )

self.Inputs = WireLib.CreateInputs(self, {
"Active"
})

self._plyCache = WireLib.Sound.NewPlayerDistanceCache(self, WireLib.Sound.VOICE_MAXDIST_SQR)
end

-- table(Entity(gmod_wire_adv_speaker), true)
self._activeSpeakers = {}

-- Callback not called if 'Active' changes right after creation.
self:OnActiveChanged(nil, nil, self:GetActive())
end

if SERVER then
function ENT:Think()
if not self:GetLive() then return end

self._plyCache:Think()
end
end

local function PlayerCanHearPlayersVoice_Hook(listener, talker)
-- Note: any given speaker can only be connected to one microphone,
-- so this loops can be considered O(nMic), not O(nMic*nSpeaker)
for _, mic in ipairs(LiveMics) do
if not mic._plyCache.PlayersInRange[talker] then goto mic_next end

Check warning on line 60 in lua/entities/gmod_wire_adv_microphone.lua

View workflow job for this annotation

GitHub Actions / lint

"Goto"

Don't use labels and gotos unless you're jumping out of multiple loops.

for speaker in pairs(mic._activeSpeakers) do
if IsValid(speaker) and speaker._plyCache.PlayersInRange[listener] then
return true, false -- Can hear, not in 3D
end
end
::mic_next::
end
end

local function Mic_SetLive(self, isLive)
if not IsValid(self) then
isLive = false
else
if self:GetLive() == isLive then return end
self:AddEFlags(EFL_FORCE_CHECK_TRANSMIT)
end

if isLive then
if LiveMics[1] == nil then -- Adding first microphone to live list
hook.Add("PlayerCanHearPlayersVoice", "Wire.AdvMicrophone", PlayerCanHearPlayersVoice_Hook)
end

if not table.HasValue(LiveMics, self) then
stepa2 marked this conversation as resolved.
Show resolved Hide resolved
table.insert(LiveMics, self)
end
else
if LiveMics[1] ~= nil and LiveMics[2] == nil then -- Removing last microphone from live list
hook.Remove("PlayerCanHearPlayersVoice", "Wire.AdvMicrophone")
end

table.RemoveByValue(LiveMics, self)
end
end

ENT.SetLive = Mic_SetLive

function ENT:UpdateTransmitState()
return self:GetLive() and TRANSMIT_ALWAYS or TRANSMIT_PVS
end

function ENT:GetLive()
return self:GetActive() and not table.IsEmpty(self._activeSpeakers)
end

function ENT:OnActiveChanged(_,_,active)
if self._activeSpeakers == nil then return end -- This happens on duplication restoration
if table.IsEmpty(self._activeSpeakers) then return end
self:SetLive(active)
end

function ENT:TriggerInput( name, value )
if name == "Active" then
self:SetActive(value ~= 0)
end
end

function ENT:SpeakerActivated(speaker)
if not IsValid(speaker) then return end

if self:GetActive() then
-- Must be updated before ._activeSpeakers are updated
self:SetLive(true)
end
self._activeSpeakers[speaker] = true
end

function ENT:SpeakerDeactivated(speaker)
if self:GetActive() then
local live = true
do
local spk = self._activeSpeakers

local k1 = next(spk)
if k1 == nil then -- No active speakers
live = false
else
local k2 = next(spk)
if k2 == nil and k1 == speaker then -- The only active speaker is 'speaker'
live = false
end
end
end

-- Must be updated before ._activeSpeakers are updated
self:SetLive(live)
end
self._activeSpeakers[speaker] = nil
end

function ENT:OnRemove()
timer.Simple(0, function()
if IsValid(self) then return end

Mic_SetLive(self, false)
end)
end

local CalculateDistanceGain = WireLib.Sound.CalculateDistanceGain

hook.Add("EntityEmitSound", "Wire.AdvMicrophone", function(snd)
for _, mic in ipairs(LiveMics) do
if IsValid(mic) then
mic:HandleSound(
snd.SoundName, snd.Volume or 1, snd.Pitch or 100, snd.SoundLevel or 75,
snd.Entity, snd.Pos, snd.DSP or 0,
"EmitSound"
)
end
end
end)

hook.Add("Wire_SoundPlay", "Wire.AdvMicrophone", function(name, pos, level, pitch, volume)
for _, mic in ipairs(LiveMics) do
if IsValid(mic) then
mic:HandleSound(
name, volume, pitch, level,
nil --[[entity]], pos, 0 --[[DSP]],
"sound.Play"
)
end
end
end)

function ENT:HandleSound(sndname, volume, pitch, sndlevel, entity, pos, dsp, emittype)
-- Prevent feedback loops
if IsValid(entity) and entity:GetClass() == "gmod_wire_adv_speaker" then return end
if pos == nil and IsValid(entity) then pos = entity:GetPos() end

if sndlevel ~= 0 and pos ~= nil then
-- Over-256 values are 'reserved for sounds using goldsrc compatibility attenuation'
-- I don't care about correct attenuation for HLSource entities,
-- but I don't want the system to break.
if sndlevel >= 256 then sndlevel = sndlevel - 256 end

volume = volume * CalculateDistanceGain(
self:GetPos():Distance(pos), sndlevel)
end

if volume < MIN_VOLUME then return end
if volume > 1 then volume = 1 end

self:ReproduceSound(sndname, volume, pitch, dsp, emittype)
end

function ENT:ReproduceSound(snd, vol, pitch, dsp, emittype)
for speaker in pairs(self._activeSpeakers) do
if IsValid(speaker) then
speaker:ReproduceSound(snd, vol, pitch, dsp, emittype)
end
end
end

-- TODO: hook into sound.PlayFile
-- TODO: hook into sound.PlayURL


duplicator.RegisterEntityClass("gmod_wire_adv_microphone", WireLib.MakeWireEnt, "Data")
110 changes: 110 additions & 0 deletions lua/entities/gmod_wire_adv_speaker.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
AddCSLuaFile()

ENT.Type = "anim"
ENT.Base = "base_wire_entity"
ENT.Author = "stpM64"
ENT.PrintName = "Wire Advanced Speaker"
ENT.Purpose = "Reproduces sounds, soundscapes and player voices listened by Advanced Microphone"
ENT.WireDebugName = "Advanced Speaker"

-- TODO: stop currently played EmitSound sounds on deactivation

function ENT:SetupDataTables()
self:NetworkVar("Bool", 0, "Active")
self:NetworkVarNotify("Active", self.OnActiveChanged)
self:NetworkVar("Entity", 0, "Microphone")
self:NetworkVarNotify("Microphone",self.OnMicrophoneChanged)
end

if SERVER then
function ENT:Initialize()
self:PhysicsInit( SOLID_VPHYSICS )
self:SetMoveType( MOVETYPE_VPHYSICS )
self:SetSolid( SOLID_VPHYSICS )

self.Inputs = WireLib.CreateInputs(self, {
"Active",
"Microphone (Must be Wire Advanced Microphone to work) [ENTITY]"
})

self:OnMicrophoneChanged(nil, nil, self:GetMicrophone())

self._plyCache = WireLib.Sound.NewPlayerDistanceCache(self, WireLib.Sound.VOICE_MAXDIST_SQR)
end

function ENT:Think()
if not self:GetActive() then return end

local mic = self:GetMicrophone()
if not IsValid(mic) or not mic:GetActive() then return end

self._plyCache:Think()
end
end

function ENT:TriggerInput( name, value )
if name == "Active" then
self:SetActive(value ~= 0)
elseif name == "Microphone" then
if not (IsValid(value) and value:GetClass() == "gmod_wire_adv_microphone") then
value = nil
end

self:SetMicrophone(value)
end
end

function ENT:OnActiveChanged(_, oldactive, active)
if oldactive == active then return end

local mic = self:GetMicrophone()
if not IsValid(mic) then return end

if active then
mic:SpeakerActivated(self)
else
mic:SpeakerDeactivated(self)
end
end

function ENT:OnMicrophoneChanged(_, oldmic, newmic)
if self:GetActive() and oldmic ~= newmic then
if IsValid(oldmic) then
oldmic:SpeakerDeactivated(self)
end

if IsValid(newmic) then
newmic:SpeakerActivated(self)
end
end
end

function ENT:OnRemove()
local mic = self:GetMicrophone()
if not IsValid(mic) then return end

timer.Simple(0, function()
if IsValid(self) or not IsValid(mic) then return end
mic:SpeakerDeactivated(self)
end)
end

function ENT:ReproduceSound(snd, vol, pitch, dsp, emittype)
if not self:GetActive() then return end

if WireLib.Sound.IsLooped(WireLib.Sound.StripPrefix(snd)) then
return
end


local soundlevel = 75
if emittype == "EmitSound" then
self:EmitSound(snd, soundlevel, pitch, vol, nil, nil, dsp)
elseif emittype == "sound.Play" then
sound.Play_NoWireHook(snd, self:GetPos(), soundlevel, pitch, vol)
else
ErrorNoHalt("Invalid emittype: ", emittype,"\n --sound ",snd)
end
end

duplicator.RegisterEntityClass("gmod_wire_adv_speaker", WireLib.MakeWireEnt, "Data")
Loading
Loading