feat: add Robot (ring modulation) and Dźwięk z tostera (8-bit bitcrusher) player audio effects
This commit is contained in:
7 files changed
+137
No files matched your search
@@ -2,10 +2,12 @@
|
|||||||
import { computed, ref, toRef, watch } from 'vue'
|
import { computed, ref, toRef, watch } from 'vue'
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
|
Bot,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Music4,
|
Music4,
|
||||||
Pause,
|
Pause,
|
||||||
Play,
|
Play,
|
||||||
|
Radio,
|
||||||
Repeat,
|
Repeat,
|
||||||
Repeat1,
|
Repeat1,
|
||||||
SkipBack,
|
SkipBack,
|
||||||
@@ -98,6 +100,8 @@ const EFFECT_ICONS: Record<string, any> = {
|
|||||||
sparkles: Sparkles,
|
sparkles: Sparkles,
|
||||||
waves: Waves,
|
waves: Waves,
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
|
bot: Bot,
|
||||||
|
radio: Radio,
|
||||||
}
|
}
|
||||||
|
|
||||||
function effectIcon(iconName: string) {
|
function effectIcon(iconName: string) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from zyrarbot.audio.effects import (
|
|||||||
DistortionEffect,
|
DistortionEffect,
|
||||||
FourthDensityEffect,
|
FourthDensityEffect,
|
||||||
MuffledEffect,
|
MuffledEffect,
|
||||||
|
RobotEffect,
|
||||||
|
ToasterEffect,
|
||||||
TremoloEffect,
|
TremoloEffect,
|
||||||
create_default_effects,
|
create_default_effects,
|
||||||
)
|
)
|
||||||
@@ -47,6 +49,23 @@ class TestAudioEffects(unittest.TestCase):
|
|||||||
out = fx.process(pcm)
|
out = fx.process(pcm)
|
||||||
self.assertEqual(len(out), len(pcm))
|
self.assertEqual(len(out), len(pcm))
|
||||||
|
|
||||||
|
def test_robot_modulates_ring(self):
|
||||||
|
fx = RobotEffect(freq_hz=65.0, mix=0.85)
|
||||||
|
pcm = (struct.pack("<hh", 10000, 10000)) * 960
|
||||||
|
out = fx.process(pcm)
|
||||||
|
self.assertEqual(len(out), len(pcm))
|
||||||
|
self.assertNotEqual(out, pcm)
|
||||||
|
|
||||||
|
def test_toaster_bitcrush_pcm(self):
|
||||||
|
fx = ToasterEffect(bit_depth=5, downsample=6)
|
||||||
|
pcm = struct.pack("<hh", 12345, -23456)
|
||||||
|
out = fx.process(pcm)
|
||||||
|
l, r = struct.unpack("<hh", out)
|
||||||
|
# Low 11 bits are zeroed due to 5-bit quantization
|
||||||
|
self.assertEqual(l & 0x07FF, 0)
|
||||||
|
self.assertEqual(r & 0x07FF, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_pipeline_chains_multiple_effects_and_applies_volume(self):
|
def test_pipeline_chains_multiple_effects_and_applies_volume(self):
|
||||||
pipeline = AudioEffectsPipeline(create_default_effects())
|
pipeline = AudioEffectsPipeline(create_default_effects())
|
||||||
pcm = (struct.pack("<hh", 1000, -1000)) * 960
|
pcm = (struct.pack("<hh", 1000, -1000)) * 960
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from zyrarbot.audio.effects.registry import (
|
|||||||
create_default_effects,
|
create_default_effects,
|
||||||
get_available_effects_metadata,
|
get_available_effects_metadata,
|
||||||
)
|
)
|
||||||
|
from zyrarbot.audio.effects.robot import RobotEffect
|
||||||
|
from zyrarbot.audio.effects.toaster import ToasterEffect
|
||||||
from zyrarbot.audio.effects.tremolo import TremoloEffect
|
from zyrarbot.audio.effects.tremolo import TremoloEffect
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -17,6 +19,8 @@ __all__ = [
|
|||||||
"DistortionEffect",
|
"DistortionEffect",
|
||||||
"FourthDensityEffect",
|
"FourthDensityEffect",
|
||||||
"MuffledEffect",
|
"MuffledEffect",
|
||||||
|
"RobotEffect",
|
||||||
|
"ToasterEffect",
|
||||||
"TremoloEffect",
|
"TremoloEffect",
|
||||||
"create_default_effects",
|
"create_default_effects",
|
||||||
"get_available_effects_metadata",
|
"get_available_effects_metadata",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from zyrarbot.audio.effects.base import AudioEffect
|
|||||||
from zyrarbot.audio.effects.distortion import DistortionEffect
|
from zyrarbot.audio.effects.distortion import DistortionEffect
|
||||||
from zyrarbot.audio.effects.fourth_density import FourthDensityEffect
|
from zyrarbot.audio.effects.fourth_density import FourthDensityEffect
|
||||||
from zyrarbot.audio.effects.muffled import MuffledEffect
|
from zyrarbot.audio.effects.muffled import MuffledEffect
|
||||||
|
from zyrarbot.audio.effects.robot import RobotEffect
|
||||||
|
from zyrarbot.audio.effects.toaster import ToasterEffect
|
||||||
from zyrarbot.audio.effects.tremolo import TremoloEffect
|
from zyrarbot.audio.effects.tremolo import TremoloEffect
|
||||||
|
|
||||||
|
|
||||||
@@ -14,6 +16,8 @@ def create_default_effects() -> list[AudioEffect]:
|
|||||||
return [
|
return [
|
||||||
MuffledEffect(),
|
MuffledEffect(),
|
||||||
FourthDensityEffect(),
|
FourthDensityEffect(),
|
||||||
|
RobotEffect(),
|
||||||
|
ToasterEffect(),
|
||||||
DistortionEffect(),
|
DistortionEffect(),
|
||||||
TremoloEffect(),
|
TremoloEffect(),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import array
|
||||||
|
import math
|
||||||
|
|
||||||
|
from zyrarbot.audio.effects.base import AudioEffect
|
||||||
|
|
||||||
|
|
||||||
|
class RobotEffect(AudioEffect):
|
||||||
|
"""Efekt metalicznego, robotycznego głosu oparty na ring modulation (modulacja pierścieniowa ~65Hz)."""
|
||||||
|
|
||||||
|
id = "robot"
|
||||||
|
name = "Robot"
|
||||||
|
description = "Metaliczny, robotyczny głos cyborga (ring modulation ~65Hz)"
|
||||||
|
badge = "CYBORG"
|
||||||
|
icon = "bot"
|
||||||
|
|
||||||
|
def __init__(self, freq_hz: float = 65.0, mix: float = 0.85) -> None:
|
||||||
|
self.freq_hz = freq_hz
|
||||||
|
self.mix = mix
|
||||||
|
self._phase = 0.0
|
||||||
|
self._phase_inc = 2.0 * math.pi * freq_hz / 48000.0
|
||||||
|
|
||||||
|
def process(self, pcm_data: bytes, sample_rate: int = 48000) -> bytes:
|
||||||
|
if not pcm_data:
|
||||||
|
return pcm_data
|
||||||
|
|
||||||
|
samples = array.array("h", pcm_data)
|
||||||
|
phase = self._phase
|
||||||
|
phase_inc = self._phase_inc
|
||||||
|
mix = self.mix
|
||||||
|
dry = 1.0 - mix
|
||||||
|
|
||||||
|
for i in range(0, len(samples), 2):
|
||||||
|
carrier = math.sin(phase)
|
||||||
|
phase += phase_inc
|
||||||
|
if phase >= 2.0 * math.pi:
|
||||||
|
phase -= 2.0 * math.pi
|
||||||
|
|
||||||
|
mod_factor = dry + mix * carrier
|
||||||
|
samples[i] = max(-32768, min(32767, int(samples[i] * mod_factor)))
|
||||||
|
samples[i + 1] = max(-32768, min(32767, int(samples[i + 1] * mod_factor)))
|
||||||
|
|
||||||
|
self._phase = phase
|
||||||
|
return samples.tobytes()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self._phase = 0.0
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import array
|
||||||
|
|
||||||
|
from zyrarbot.audio.effects.base import AudioEffect
|
||||||
|
|
||||||
|
|
||||||
|
class ToasterEffect(AudioEffect):
|
||||||
|
"""Efekt 'dźwięku z tostera / mikrofalówki' – memiczny lo-fi bitcrusher (5-bit quant + downsampling)."""
|
||||||
|
|
||||||
|
id = "toaster"
|
||||||
|
name = "Dźwięk z tostera"
|
||||||
|
description = "Memiczny dźwięk z mikrofalówki / kalkulatora (bitcrusher 5-bit, aliasing)"
|
||||||
|
badge = "8-BIT"
|
||||||
|
icon = "radio"
|
||||||
|
|
||||||
|
def __init__(self, bit_depth: int = 5, downsample: int = 6) -> None:
|
||||||
|
self.bit_depth = bit_depth
|
||||||
|
self.downsample = downsample
|
||||||
|
self.shift = 16 - bit_depth
|
||||||
|
self._counter = 0
|
||||||
|
self._hold_l = 0
|
||||||
|
self._hold_r = 0
|
||||||
|
|
||||||
|
def process(self, pcm_data: bytes, sample_rate: int = 48000) -> bytes:
|
||||||
|
if not pcm_data:
|
||||||
|
return pcm_data
|
||||||
|
|
||||||
|
samples = array.array("h", pcm_data)
|
||||||
|
shift = self.shift
|
||||||
|
down = self.downsample
|
||||||
|
hold_l = self._hold_l
|
||||||
|
hold_r = self._hold_r
|
||||||
|
cnt = self._counter
|
||||||
|
|
||||||
|
for i in range(0, len(samples), 2):
|
||||||
|
if cnt == 0:
|
||||||
|
# Obniżenie rozdzielczości bitowej (bitcrush / kwantyzacja do 5 bitów)
|
||||||
|
sl = samples[i]
|
||||||
|
sr = samples[i + 1]
|
||||||
|
hold_l = (sl >> shift) << shift
|
||||||
|
hold_r = (sr >> shift) << shift
|
||||||
|
|
||||||
|
samples[i] = hold_l
|
||||||
|
samples[i + 1] = hold_r
|
||||||
|
cnt = (cnt + 1) % down
|
||||||
|
|
||||||
|
self._counter = cnt
|
||||||
|
self._hold_l = hold_l
|
||||||
|
self._hold_r = hold_r
|
||||||
|
return samples.tobytes()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self._counter = 0
|
||||||
|
self._hold_l = 0
|
||||||
|
self._hold_r = 0
|
||||||
@@ -117,6 +117,8 @@ class ControlCog(MusicCogBase, name="Control"):
|
|||||||
nazwa=[
|
nazwa=[
|
||||||
app_commands.Choice(name="Przester (1000%)", value="distortion"),
|
app_commands.Choice(name="Przester (1000%)", value="distortion"),
|
||||||
app_commands.Choice(name="Czwarta gęstość (~1s pitch)", value="fourth_density"),
|
app_commands.Choice(name="Czwarta gęstość (~1s pitch)", value="fourth_density"),
|
||||||
|
app_commands.Choice(name="Robot (cyborg voice)", value="robot"),
|
||||||
|
app_commands.Choice(name="Dźwięk z tostera (8-bit lo-fi)", value="toaster"),
|
||||||
app_commands.Choice(name="Pod wodą (lowpass)", value="muffled"),
|
app_commands.Choice(name="Pod wodą (lowpass)", value="muffled"),
|
||||||
app_commands.Choice(name="Tremolo (pulsowanie)", value="tremolo"),
|
app_commands.Choice(name="Tremolo (pulsowanie)", value="tremolo"),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in new issue
Block a user