COPY THE MODULE HERE
const RunService = game:GetService("RunService")
export type Oscillator = {
frequency : number,
volume : number,
phase : number,
--
filter : string,
cutoff : number,
filterType : string, -- not gonna implement these
}
local Synth = {sounds = {}}
Synth.oscillator = function(frequence, volume, phase, parent, sound, BaseFrequency)
if not sound then
sound = Instance.new("Sound")
sound.SoundId = 'rbxassetid://109951318532992' -- 440 Hertz base audio, change it to your own audio id if needed
end
local baseFreq = BaseFrequency or 440
local targetFreq = math.max(frequence or baseFreq, 20)
local clampedVolume = math.clamp(volume or 0, 0, 1)
sound.Volume = clampedVolume
sound.PlaybackSpeed = targetFreq / baseFreq
sound.Looped = true
sound.Parent = parent
--sound.TimePosition = ((phase or 0) / (2 * math.pi)) / math.max(baseFreq, 0.0001) -- disable phase here too i guess??
sound:Play()
return sound
end
Synth.play = function(duration, oscillator_func : (time : number, duration : number) -> {Oscillator}, soundName, BaseFrequency)
soundName = soundName or 'Sound-'..duration..'-'..math.random(0, 10000)
if not game.ReplicatedStorage:FindFirstChild('SynthSounds') then
local folder = Instance.new("Folder", game.ReplicatedStorage)
folder.Name = 'SynthSounds'
end
local SoundFolder = Instance.new("Folder", game.ReplicatedStorage.SynthSounds)
SoundFolder.Name = soundName
local this = {
duration = duration,
oscillator_func = oscillator_func,
start = os.clock(),
oscillators = {},
time = 0,
BaseFrequency = BaseFrequency or 440,
BaseID = 'rbxassetid://109951318532992'
}
this.Stop = function(self)
for i, v in self.oscillators do
v:Stop()
v:Destroy()
self.oscillators[i] = nil
end
SoundFolder:Destroy()
if self.onUpdate then
self.onUpdate:Disconnect()
self.onUpdate = nil
end
Synth.sounds[soundName] = nil
end
this.onUpdate = RunService.Heartbeat:Connect(function(dt)
this.time = os.clock() - this.start
if this.time >= this.duration then
this:Stop()
return
end
local oscillators = this.oscillator_func(this.time, this.duration)
if #this.oscillators > #oscillators then
for i = #oscillators + 1, #this.oscillators do
this.oscillators[i]:Destroy()
this.oscillators[i] = nil
end
end
for order, oscillation : Oscillator in oscillators do
local sound = this.oscillators[order]
local frequency = math.max(oscillation.frequency or 0, 20)
local volume = math.max(oscillation.volume or 0, 0)
if not sound then
sound = Synth.oscillator(frequency, volume, oscillation.phase, SoundFolder, nil, this.BaseFrequency)
this.oscillators[order] = sound
else
sound.PlaybackSpeed = frequency / this.BaseFrequency
sound.Volume = volume
if oscillation.phase ~= nil then -- disabled phasing because it makes music sound ugly, uncomment to re-enable
--sound.TimePosition = ((oscillation.phase % (2 * math.pi)) / (2 * math.pi)) / math.max(this.BaseFrequency, 0.0001)
end
if not sound.Playing then sound:Play() end
end
sound.Name = order
sound.SoundId = this.BaseID
end
end)
Synth.sounds[soundName] = this
return this
end
-- extras
Synth.square = function(frequency, volume)
local oscillators = {}
for harmonic = 1, 15, 2 do
table.insert(oscillators, {
frequency = frequency * harmonic,
volume = volume / harmonic
})
end
return oscillators
end
Synth.saw = function(frequency, volume)
local oscillators = {}
for harmonic = 1, 20 do
table.insert(oscillators, {
frequency = frequency * harmonic,
volume = volume / harmonic,
})
end
return oscillators
end
Synth.triangle = function(frequency, volume)
local oscillators = {}
for harmonic = 1, 20, 2 do
table.insert(oscillators, {
frequency = frequency * harmonic,
volume = volume / (harmonic * harmonic),
phase = harmonic % 4 == 1 and 0 or math.pi,
})
end
return oscillators
end
Synth.adsr = function(t, duration, attack, decay, sustain, release) -- not sure if this is right, i saw it on a video and i wanted to add it
attack = math.max(attack or 0, 0)
decay = math.max(decay or 0, 0)
release = math.max(release or 0, 0)
sustain = math.clamp(sustain or 0, 0, 1)
if attack + decay + release > duration then
local scale = duration / (attack + decay + release)
attack *= scale
decay *= scale
release *= scale
end
if attack > 0 and t < attack then
return t / attack
elseif decay > 0 and t < attack + decay then
local alpha = (t - attack) / decay
return 1 + (sustain - 1) * alpha
elseif t < duration - release then
return sustain
elseif release > 0 then
local alpha = (t - (duration - release)) / release
return sustain * (1 - alpha)
else
return 0
end
end
return Synth
i’ve had the idea to make this after seeing a video of someone making a game entirely out of sinewaves so i thought it’d be great to make a module that layers different frequencies to make different audios,
i wouldn’t exactly call this EditableAudio but it’s the closest thing to what it actually is lol, this module is VERY simple though + the layered sounds may not play at the same time together so stuff like pops might sound like beeps.
- it uses a 440 Hz sound as a base, it changes the
PlaybackSpeedof the audio to make it sound like different frequencies, so a 880 Hz audio is a 440 Hz audio with a PlaybackSpeed of 2
Example Script + Video
Example Script
const Synth = require(game.ReplicatedStorage:WaitForChild("Synth"))
local sound
game.UserInputService.InputEnded:Connect(function(i, gpe)
if i.KeyCode ~= Enum.KeyCode.Space then return end
if sound then
sound:Stop()
end
sound = Synth.play(5, function(t, duration)
return {
{
frequency = 200 + 200 * t / duration,
volume = 0.8 * (1 - t / duration)
},
{
frequency = 100 + 100 * t / duration,
volume = 0.6 * (1 - t / duration)
},
{
frequency = 600 - 50 * t / duration,
volume = 0.8 * (1 - t / duration)
},
{
frequency = 400 - 100 * math.exp(t / duration),
volume = 0.6 * (1 - t / duration)
}
}
end, 'Sound1')
end)
Showcase of Stuff i did with it:
can be found in this post Sound Synth Module Showcase