Sound Synth Module (EditableAudio)

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 PlaybackSpeed of 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

11 Likes

this is sick, I’ve had this concept as “probably possible” in the back of my head for awhile and it’s cool seeing it actually made!

2 Likes