class Volume:
    """Server volume controls"""
    def __init__(self,
                 server: "SCServer",
                 min_: int = -90,
                 max_: int = 6) -> None:
        self._server = server
        self._server.add_init_hook(self.send_synthdef)
        self._server.add_init_hook(self.update_synth)

        self.min = min_
        self.max = max_

        self._muted = False
        self._volume = 0.0
        self._lag = 0.1

        self._synth_name: Optional[str] = None

        self._synth: Optional[Synth] = None

    @property
    def muted(self):
        """True if muted."""
        return self._muted

    @muted.setter
    def muted(self, muted: bool):
        if muted:
            self.mute()
        else:
            self.unmute()

    @property
    def volume(self):
        """Volume in dB."""
        return self._volume

    @volume.setter
    def volume(self, volume):
        self._volume = clip(volume, self.min, self.max)
        self.update_synth()

    def mute(self) -> None:
        """Mute audio"""
        self._muted = True
        self.update_synth()

    def unmute(self) -> None:
        """Unmute audio"""
        self._muted = False
        self.update_synth()

    def update_synth(self) -> None:
        """Update volume Synth"""
        amp = 0.0 if self._muted else dbamp(self._volume)
        active = amp != 1.0
        if active:
            if self._server.is_running:
                if self._synth is None:
                    if self._synth_name is None:
                        warnings.warn(
                            "Cannot set volume. Volume SynthDef unknown. Is the default sclang running?"
                        )
                        return
                    controls = {
                        "volumeAmp": amp,
                        "volumeLag": self._lag,
                        "bus": self._server.output_bus.idxs[0],
                    }
                    self._synth = Synth(
                        self._synth_name,
                        add_action=AddAction.AFTER,
                        target=self._server.default_group,
                        controls=controls,
                        server=self._server,
                    )
                else:
                    self._synth.set("volumeAmp", amp)
        else:
            if self._synth is not None:
                self._synth.release()
                self._synth = None

    def send_synthdef(self):
        """Send Volume SynthDef"""
        if self._server.is_running:
            num_channels = self._server.output_bus.num_channels
            synth_def = SynthDef(
                f"sc3nb_volumeAmpControl{num_channels}",
                r"""{ | volumeAmp = 1, volumeLag = 0.1, gate=1, bus |
                    XOut.ar(bus,
                        Linen.kr(gate, releaseTime: 0.05, doneAction:2),
                        In.ar(bus, ^num_channels) * Lag.kr(volumeAmp, volumeLag)
                    );
                }""",
            )
            try:
                self._server.lookup_receiver("sclang")
            except KeyError:
                _LOGGER.info(
                    "Volume SynthDef cannot be send. No sclang receiver known."
                )
            else:
                self._synth_name = synth_def.add(server=self._server)
                assert self._synth_name is not None, "Synth name is None"
class SynthTest(SCBaseTest):

    __test__ = True

    def setUp(self) -> None:
        with self.assertRaises(RuntimeError):
            SynthTest.sc.lang
        self.custom_nodeid = 42
        self.synth_args = {"amp": 0.0, "num": 3}
        warnings.simplefilter("always", UserWarning)
        with self.assertWarnsRegex(UserWarning,
                                   "SynthDesc 's2' is unknown",
                                   msg="SynthDesc seems to be known"):
            self.synth = Synth("s2",
                               controls=self.synth_args,
                               nodeid=self.custom_nodeid)
        self.assertIsNone(self.synth._synth_desc)
        self.sc.server.sync()

    def tearDown(self) -> None:
        nodeid = self.synth.nodeid
        self.assertIn(nodeid, self.sc.server.nodes)
        self.synth.free()
        self.synth.wait()
        del self.synth  # make sure that synth is deleted from registry
        t0 = time.time()
        while nodeid in self.sc.server.nodes:
            time.sleep(0.005)
            if time.time() - t0 > 0.2:
                self.fail("NodeID is still in server.nodes")
        self.assertNotIn(nodeid, self.sc.server.nodes)
        with self.assertRaises(KeyError):
            del self.sc.server.nodes[nodeid]

    def test_node_registry(self):
        copy1 = Synth(nodeid=self.synth.nodeid, new=False)
        copy2 = Synth(nodeid=self.custom_nodeid, new=False)
        self.assertIs(self.synth, copy1)
        self.assertIs(self.synth, copy2)
        self.assertIs(copy1, copy2)
        del copy1, copy2

    def test_set_get(self):
        for name, value in {"amp": 0.0, "num": 1}.items():
            self.synth.__setattr__(name, value)
            self.assertAlmostEqual(self.synth.__getattr__(name), value)

        with self.assertWarnsRegex(UserWarning,
                                   "Setting 'freq' as python attribute"):
            with self.assertRaisesRegex(AttributeError, "no attribute 'freq'"):
                self.synth.__getattribute__(
                    "freq")  # should not have a python attribute named freq
            self.synth.freq = 420  # should warn if setting attribute

        self.assertAlmostEqual(self.synth.get("freq"),
                               400)  # default freq of s2 SynthDef

        with self.assertWarnsRegex(UserWarning,
                                   "recognized as Node Parameter now"):
            self.synth.set("freq", 100)
        self.assertAlmostEqual(self.synth.get("freq"), 100)

        self.synth.freq = 300
        self.assertAlmostEqual(self.synth.get("freq"), 300)

        for name, value in self.synth_args.items():
            self.assertAlmostEqual(self.synth.__getattr__(name), value)

    def test_new_warning(self):
        with self.assertLogs(level="WARNING") as log:
            self.synth.new()
            time.sleep(0.1)
        self.assertTrue("duplicate node ID" in log.output[-1])

    def test_query(self):
        query_result = self.synth.query()
        self.assertIsInstance(query_result, SynthInfo)
        self.assertEqual(query_result.nodeid, self.custom_nodeid)
        self.assertEqual(query_result.group,
                         SynthTest.sc.server.default_group.nodeid)
        self.assertEqual(query_result.prev_nodeid, -1)
        self.assertEqual(query_result.next_nodeid, -1)