class PWMEffect(FixedLevelEffect):
    'Interrupt setup/routine alternates fixed level between fixedreg and zero.'

    level4toshape = level4totone5shape class Lfsr:

    def __init__(self, nzdegrees):
        self.mask = sum(1 << (nzd - 1) for nzd in nzdegrees)
        self.x = 1

    def __call__(self):
        bit = self.x & 1
        self.x >>= 1
        if bit:
            self.x ^= self.mask
        return 1 - bit # Authentic, see qnoispec.

    def __iter__(self):
        first = self.x
        while True:
            yield self()
            if first == self.x:
                break class MFPTimer:
    def __init__(self):
    def update(self, tcr, tdr, effect):
    def _findtcrtdr(self, freq):
    def _getnormperiod(self):
    def getfreq(self): # Currently only called when effect is not None. class Block:

    def __init__(self, framecount):
        # If it's a numpy type it can cause overflow in osc module, so ensure arbitrary precision:
        self.framecount = int(framecount)

    def __repr__(self):
        return f"{type(self).__name__}({self.framecount!r})" class EnvOsc(ShapeOsc):

    eager = True
    steps = 32
    shapes = {
        0x0c: Shape(range(steps)),
        0x08: Shape(range(steps - 1, -1, -1)),
        0x0e: Shape(itertools.chain(range(steps), range(steps - 1, -1, -1))),
        0x0a: Shape(itertools.chain(range(steps - 1, -1, -1), range(steps))),
        0x0f: Shape(itertools.chain(range(steps), steps * [0]), steps),
        0x0d: Shape(itertools.chain(range(steps), steps * [steps - 1]), steps),
        0x0b: Shape(itertools.chain(range(steps - 1, -1, -1), steps * [steps - 1]), steps),
        0x09: Shape(itertools.chain(range(steps - 1, -1, -1), steps * [0]), steps),
    }
    for s in range(0x08):
        shapes[s] = shapes[0x0f if s & 0x04 else 0x09]
    del s

    def __init__(self, scale, periodreg, shapereg):
        scaleofstep = scale * 32 // self.steps
        super().__init__(scaleofstep, periodreg)
        self.shapeversion = None
        self.shapereg = shapereg

    def callimpl(self):
        if self.shapeversion != self.shapereg.version:
            self.reset(self.shapes[self.shapereg.value])
            self.shapeversion = self.shapereg.version
        super().callimpl() class NoiseOsc(ShapeOsc):

    eager = False

    def __init__(self, scale, periodreg, shape):
        scaleofstep = scale * 2 # This results in authentic spectrum, see qnoispec.
        super().__init__(scaleofstep, periodreg)
        self.reset(shape) class RToneOsc(BufNode):

    effect = None

    def __init__(self, mfpclock, chipimplclock, timer, fixedreg):
        super().__init__(BufType.signal)
        self.mfpclock = mfpclock
        self.chipimplclock = chipimplclock
        self.timer = timer
        self.fixedreg = fixedreg

    def callimpl(self):
        effect = self.timer.effect.value
        if effect is not self.effect:
            # Simulate running interrupt routine synchronously now (start of frame) and subsequently by interrupt:
            self.maincounter = 0
            self.precounterxmfp = None
            self.repeat, self.index = 0, -1 # Reset index in shape.
            self.effect = effect
        self.shape = effect.getshape(self.fixedreg)
        prescalerornone = self.timer.prescalerornone.value
        if prescalerornone is None:
            self.blockbuf.fill_same(self.shape.buf[self.index])
            self.precounterxmfp = None
        else:
            if self.precounterxmfp is None:
                self.precounterxmfp = prescalerornone * self.chipimplclock
            self.repeat, self.index, self.maincounter, self.precounterxmfp = self.rtoneimpl(prescalerornone, self.timer.effectivedata.value)
        self.timer.repeat.value = self.repeat

    @turbo(
        self = dict(
            oscnodepyrbotype,
            mfpclock = i8, # Signed so j doesn't blow up.
            chipimplclock = u8,
            maincounter = i4,
            precounterxmfp = u4,
            repeat = u4,
        ),
        prescaler = u4,
        etdr = u4,
        chunksizexmfp = u8,
        stepsizexmfp = u8,
        nextstepxmfp = i8,
        i = u4,
        j = u4,
        val = signaldtype,
        numerator = i8,
    )
    def rtoneimpl(self, prescaler, etdr):
        self_blockbuf_buf = self_block_framecount = self_mfpclock = self_chipimplclock = self_repeat = self_index = self_maincounter = self_precounterxmfp = self_shape_buf = self_shape_size = self_shape_introlen = LOCAL
        chunksizexmfp = self_chipimplclock * prescaler
        stepsizexmfp = chunksizexmfp * etdr
        nextstepxmfp = chunksizexmfp * self_maincounter + self_precounterxmfp - chunksizexmfp
        i = 0
        while True:
            numerator = nextstepxmfp + self_mfpclock - 1
            if numerator >= 0:
                j = min(numerator // self_mfpclock, self_block_framecount)
                val = self_shape_buf[self_index]
                while i < j:
                    self_blockbuf_buf[i] = val
                    i += 1
                if j == self_block_framecount:
                    break
            nextstepxmfp += stepsizexmfp
            self_index += 1
            if self_index == self_shape_size:
                self_repeat += 1
                self_index = self_shape_introlen
        nextstepxmfp -= self_mfpclock * self_block_framecount
        self_maincounter = 1
        while nextstepxmfp < 0:
            nextstepxmfp += chunksizexmfp
            self_maincounter -= 1
        self_maincounter += nextstepxmfp // chunksizexmfp
        return self_repeat, self_index, self_maincounter, nextstepxmfp % chunksizexmfp class ToneOsc(ShapeOsc):

    eager = True

    def __init__(self, scale, periodreg):
        scaleofstep = scale * 2 // 2 # Normally half of 16.
        super().__init__(scaleofstep, periodreg)
        self.reset(toneshape) class Reg:

    undefined = object()

    def __init__(self, value = undefined, *, maxval = None, minval = None):
        self.links = []
        self.idle = True
        self.maxval = maxval # Friendlier than a mask.
        self.minval = minval # Typically to avoid 0 period.
        if value is not self.undefined:
            self.value = value

    def link(self, xform, *upstream):
        link = Link(self, xform, upstream)
        for r in upstream:
            r.links.append(link)
        return self

    def mlink(self, mask, xform, *upstream):
        negmask = ~mask
        self.link(lambda *args: (negmask & self.value) | (mask & xform(*args)), *upstream)

    def set(self, value):
        if self.maxval is not None:
            value = min(self.maxval, value)
        if self.minval is not None:
            value = max(self.minval, value)
        self._value = value
        self.idle = False
        try:
            for link in self.links:
                link.update()
        finally:
            self.idle = True

    value = property(lambda self: self._value, lambda self, value: self.set(value)) class VersionReg(Reg):

    version = 0

    def set(self, value):
        super().set(value)
        self.version += 1 def level5toamp(level):
def _amptolevel4(amp):
def level4to5(level4):
    def level4to5(cls, data4, introlen = defaultintrolen):
    def __init__(self, g, introlen = defaultintrolen):
    def wavelength(self):
def _meansin(x1, x2):
def _sinsliceamp(i, n, skew):
def _sinuslevel4(steps, maxlevel4, skew):
def makesample5shape(data, signed, is4bit):
class Shape: TP = lambda f, r: ((r & 0x0f) << 8) | f
NP = lambda p: p & 0x1f
EP = lambda f, r: (r << 8) | f
class MixerFlag:
class LogicalRegisters:
class PhysicalRegisters:
class YM2149(Container):
    def __init__(self, bit):
    def __call__(self, m):
    def __init__(self, config, clockinfo, minnoiseperiod = 1):
    def __init__(self, config, logical):
    def __init__(self, config, clockinfo, ampscale, logical):
    def callimpl(self):
from .dac import PWMEffect
from .lfsr import Lfsr
from .mfp import mfpclock
from .nod import Block
from .osc2 import EnvOsc, NoiseOsc, RToneOsc, ToneOsc
from .reg import Reg, VersionReg
from .shapes import toneshape
from .ym2149 import YM2149, ym2149nzdegrees
from types import SimpleNamespace
from unittest import TestCase
import sys, time
#
# pym2149 is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pym2149 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pym2149.  If not, see <http://www.gnu.org/licenses/>.


class CmpTime:

    def cmptime(self, start, strictlimit):
        expression = f"{time.time() - start:.3f} < {strictlimit}"
        sys.stderr.write(f"{expression} ... ")
        self.assertTrue(eval(expression)) # XXX: Really use rounded time taken?

class AbstractTestOsc(CmpTime):

    def test_performance(self):
        blockrate = 50
        blocksize = 2000000 // blockrate
        for p in self.performanceperiods:
            o = self.createperfosc(8, p)
            start = time.time()
            for _ in range(blockrate):
                o.call(Block(blocksize))
            self.cmptime(start, self.performancelimit)

class TestToneOsc(AbstractTestOsc, TestCase):

    performanceperiods = 0x001, 0xfff
    performancelimit = .05

    @classmethod
    def createperfosc(cls, scale, period):
        return cls.createosc(scale, Reg(value = period))

    @staticmethod
    def createosc(scale, periodreg):
        return ToneOsc(scale, periodreg)

    def test_works(self):
        o = self.createosc(8, Reg(value = 3))
        v = o.call(Block(96)).tolist()
        self.assertEqual([1] * 24, v[:24])
        self.assertEqual([0] * 24, v[24:48])
        self.assertEqual([1] * 24, v[48:72])
        self.assertEqual([0] * 24, v[72:])
        v = o.call(Block(48)).tolist()
        self.assertEqual([1] * 24, v[:24])
        self.assertEqual([0] * 24, v[24:])

    def test_resume(self):
        o = self.createosc(8, Reg(value = 3))
        v = o.call(Block(25)).tolist()
        self.assertEqual([1] * 24, v[:24])
        self.assertEqual([0], v[24:])
        v = o.call(Block(24)).tolist()
        self.assertEqual([0] * 23, v[:23])
        self.assertEqual([1], v[23:])

    def test_carry(self):
        r = Reg(value = 0x01)
        size = 3 * 8 + 1
        ref = self.createosc(8, r).call(Block(size)).tolist()
        for n in range(size + 1):
            o = self.createosc(8, r)
            v1 = o.call(Block(n)).tolist()
            v2 = o.call(Block(size - n)).tolist()
            self.assertEqual(ref, v1 + v2)

    def test_endexistingstepatendofblock(self):
        r = Reg(value = 0x01)
        o = self.createosc(8, r)
        self.assertEqual([1] * 4, o.call(Block(4)).tolist())
        self.assertEqual([1] * 4, o.call(Block(4)).tolist())
        self.assertEqual([0] * 4, o.call(Block(4)).tolist())

    def test_increaseperiodonboundary(self):
        r = Reg(value = 0x01)
        o = self.createosc(8, r)
        self.assertEqual([1] * 8 + [0] * 8, o.call(Block(16)).tolist())
        r.value = 0x02
        self.assertEqual([0] * 8 + [1] * 16 + [0] * 15, o.call(Block(39)).tolist())
        r.value = 0x03
        self.assertEqual([0] * 9 + [1] * 24 + [0], o.call(Block(34)).tolist())

    def test_decreaseperiodonboundary(self):
        r = Reg(value = 0x03)
        o = self.createosc(8, r)
        self.assertEqual([1] * 24 + [0] * 24, o.call(Block(48)).tolist())
        r.value = 0x02
        self.assertEqual([1] * 16 + [0] * 16 + [1] * 6, o.call(Block(38)).tolist())
        r.value = 0x01
        self.assertEqual([1] * 2 + [0] * 8 + [1] * 8 + [0], o.call(Block(19)).tolist())

    def test_smallerblocksthanperiod(self):
        r = Reg(value = 0x05)
        o = self.createosc(1, r)
        self.assertEqual([1,1,1,1], o.call(Block(4)).tolist())
        self.assertEqual([1,0,0,0], o.call(Block(4)).tolist())
        self.assertEqual([0,0,1], o.call(Block(3)).tolist())
        self.assertEqual([1,1,1,1], o.call(Block(4)).tolist())
        self.assertEqual([0,0,0,0,0], o.call(Block(5)).tolist())
        self.assertEqual([1], o.call(Block(1)).tolist())

class TestRToneOsc(AbstractTestOsc, TestCase): # FIXME: MFP timers do not behave like YM2149 tones.

    performanceperiods = 0x001, 0xfff
    performancelimit = .1

    @classmethod
    def createperfosc(cls, scale, period):
        clock = 200

        effectivedata = scale*period*mfpclock//clock