def readbytecode(f, findlabel):
    bytecode = None
    for line in map(Line, f):
        if bytecode is None and findlabel == line.label: # XXX: Support terminating colon?
            bytecode = BytecodeBuilder(False) # And fall through to next clause if there is a directive.
        if bytecode is not None and line.directive is not None:
            bytecode.process(line)
            if bytecode.hasterminator():
                break
    if bytecode is None:
        raise NoSuchLabelException(findlabel)
    return bytecode.bytes class ClockInfo:

    trishapes = frozenset([0xa, 0xe])
    tonescale = 16

    @classmethod
    def _shapescale(cls, shape):
        # Musically, the triangular shapes have twice the scale:
        return 512 if shape in cls.trishapes else 256

    @types(Config, Platform, YMFile)
    def __init__(self, config, platform, ymfile = None):
        self.nomclock = config.nominalclock
        if self.nomclock % config.underclock:
            raise Exception(f"Clock {self.nomclock} not divisible by underclock {config.underclock}.")
        self.implclock = self.nomclock // config.underclock
        if ymfile is not None and self.nomclock != ymfile.nominalclock:
            log.info("Context clock %s overridden to: %s", ymfile.nominalclock, self.nomclock)
        if self.implclock != self.nomclock:
            log.debug("Clock adjusted to %s to take advantage of non-trivial underclock.", self.implclock)
        if config.underclock < 1 or defaultscale % config.underclock:
            raise Exception(f"underclock must be a factor of {defaultscale}.")
        self.scale = defaultscale // config.underclock
        if config.freqclamp:
            # The 0 case just means that 1 is audible:
            self.mintoneperiod = max(1, self._toneperiodclampor0(platform.outputrate))
            log.debug("Minimum tone period: %s", self.mintoneperiod)
        else:
            self.mintoneperiod = 1

    def _toneperiodclampor0(self, outrate):
        # Largest period with frequency strictly greater than Nyquist, or 0 if there isn't one:
        return (self.implclock - 1) // (self.scale * outrate)

    def _convert(self, freqorperiod, scale):
        return self.nomclock / (scale * freqorperiod)

    def toneperiod(self, freq):
        return self._convert(freq, self.tonescale)

    def noiseperiod(self, freq):
        return self._convert(freq, 16) # First notch at freq.

    def envperiod(self, freq, shape):
        return self._convert(freq, self._shapescale(shape))

    def tonefreq(self, period):
        return self._convert(period, self.tonescale)

    def envfreq(self, period, shape):
        return self._convert(period, self._shapescale(shape)) class ConfigName:

    namespace = 'pym2149'

    def __init__(self, *params, args = sys.argv[1:], name = 'defaultconf'):
        parser = ArgumentParser()
        parser.add_argument('--repr', action = 'append', default = [])
        parser.add_argument('--config', action = 'append', default = [])
        parser.add_argument('--ignore-settings', action = 'store_true')
        for param in params:
            parser.add_argument(param)
        self.additems = parser.parse_args(args)
        self.path = Path(__file__).resolve().parent / f"{name}.arid"

    @types(DI, this = Config)
    def loadconfig(self, di):
        config = ConfigCtrl()
        config.put('enter', function = enter)
        config.put('py', function = lambda *args: py(getattr(config.node, self.namespace), *args))
        config.put('resolve', function = lambda *args: AsScope.resolve(di, *args))
        config.printf("cwd = %s", self.path.parent)
        config.printf("%s . %s", self.namespace, self.path.name)
        if not self.additems.ignore_settings:
            try:
                config.loadsettings()
            except FileNotFoundError as e:
                log.warning("Settings not found: %s", e)
        for name, value in self.additems.__dict__.items():
            if 'config' == name:
                with config.repl() as repl:
                    for text in value:
                        repl.printf("%s", self.namespace)
                        for line in text.splitlines():
                            repl(f"\t{line}")
            else:
                setattr(getattr(config.node, self.namespace), name, value)
        return getattr(config.node, self.namespace) class Bytecode(Prerecorded):

    pianorollheight = refreshrate

    def __init__(self, bytes, extraseconds):
        self.bytes = bytes
        self.extraseconds = extraseconds

    def frames(self, chip):
        yield from _dosoundimpl(self.bytes, chip)
        if self.extraseconds:
            n = math.ceil(self.extraseconds * refreshrate)
            log.info("Streaming %.3f extra seconds.", n / refreshrate)
            yield from range(n) class Config(lurlene.iface.Config): pass @types(Config, Context, this = Started)
def loadcontext(config, context):
    with open(config.inpath) as f:
        context.update(f.read())
    context.flip() class LurleneBridge(LiveCodingBridge, Prerecorded): pass class YM2149Chip(Chip):

    param = 'ym'

    @types(Config, LogicalRegisters, ClockInfo, Tuning)
    def __init__(self, config, chip, clock, tuning):
        chans = range(config.chipchannels)
        chipregs = ChipRegs(chip, clock, tuning)
        chanproxies = [ChanProxy(chip, chan, clock, tuning) for chan in chans]
        self.channels = [ChipProxy(chip, chan, chanproxies, chipregs, clock) for chan in chans] class StereoInfo:

    @types(Config)
    def __init__(self, config):
        self.n = config.chipchannels
        if config.stereo:
            self.panlaw = config.panlaw
            self.maxpan = float(config.maxpan)
            self.getoutchans = self.getstaticoutchans
        else:
            self.getoutchans = self.gettrivialoutchans

    def pantoamp(self, outchan, pan):
        return ((1 + (outchan * 2 - 1) * pan) / 2) ** (self.panlaw / 6)

    def staticamp(self, outchan, chipchan):
        return self.pantoamp(outchan, (chipchan * 2 - (self.n - 1)) / (self.n - 1) * self.maxpan)

    def gettrivialoutchans(self):
        return [TrivialOutChannel]
    gettrivialoutchans.size = 1

    def getstaticoutchans(self):
        return [StaticOutChannel([self.staticamp(oc, cc) for cc in range(self.n)]) for oc in range(2)]
    getstaticoutchans.size = 2 class YMStream(FloatStream):

    streamname = 'ym'

    @types(Config, ClockInfo, YM2149, AmpScale, StereoInfo, MinBleps)
    def __init__(self, config, clockinfo, chip, ampscale, stereoinfo, minbleps):
        naives = [IdealMixer(chip, ampscale.log2maxpeaktopeak, outchan) for outchan in stereoinfo.getoutchans()]
        for naive in naives:
            self.append(WavBuf(clockinfo, naive, minbleps)) class ChipTimer(MinBlockRateTimer):

    @types(ClockInfo)
    def __init__(self, clockinfo):
        super().__init__(clockinfo.implclock, 100) class SimpleChipTimer(SimpleTimer):
    'One block per update.'

    @types(ClockInfo)
    def __init__(self, clockinfo):
        super().__init__(clockinfo.implclock) class SyncTimer(SimpleTimer):
    'Fill platform buffer most efficiently.'

    @types(Platform, MinBleps, ClockInfo)
    def __init__(self, platform, minbleps, clockinfo):
        self.naiverate = clockinfo.implclock
        super().__init__(self.naiverate)
        self.buffersize = platform.buffersize
        self.naivex = 0
        self.bufferx = 0
        self.minbleps = minbleps

    def blocksforperiod(self, refreshrate):
        wholeperiodblock, = super().blocksforperiod(refreshrate)
        naiveN = wholeperiodblock.framecount
        while naiveN:
            naiven = min(naiveN, self.minbleps.getminnaiven(self.naivex, self.buffersize - self.bufferx))
            yield Block(naiven)
            self.bufferx = (self.bufferx + self.minbleps.getoutcount(self.naivex, naiven)) % self.buffersize
            self.naivex = (self.naivex + naiven) % self.naiverate
            naiveN -= naiven def initlogging():
    logging.basicConfig(format = "[%(levelname)s] %(message)s", level = logging.DEBUG) class MainThread:

    @types(Config, MainBackground)
    def __init__(self, config, player):
        self.profile = config.profile
        self.trace = config.trace
        self.player = player

    def sleep(self):
        if self.profile or self.trace:
            sleeptime = self.profile.time if self.profile else self.trace
            log.debug("Continue for %.3f seconds.", sleeptime)
            time.sleep(sleeptime)
            log.debug('End of profile, shutting down.')
        else:
            log.debug('Continue until end of data or interrupt.')
            try:
                self.player.future.result()
                log.debug('End of data, shutting down.')
            except KeyboardInterrupt:
                log.debug('Caught interrupt, shutting down.') class NullRoll(Roll):

    def update(self):
        pass class RollImpl(Roll):

    line = 0
    shapes = ('\\_',) * 4 + ('/_',) * 4 + ('\\\\', '\\_', '\\/', '\\\u203e', '//', '/\u203e', '/\\', '/_')
    shapeversion = None
    wavestartstr = '~'

    @types(Config, ClockInfo, LogicalRegisters, Tuning)
    def __init__(self, config, clock, chip, tuning):
        self.height = config.pianorollheight
        self.channels = config.chipchannels
        self.periods = config.showperiods # TODO LATER: Command line arg isn't converted to boolean.
        self.stream = config.rollstream
        self.mincents = config.rollmincents
        self.jump = f"\x1b[{self.height}A"
        self.format = ' | '.join(self.channels * ["%7s %1s %2s %1s %2s%1s%7s"])
        self.effects = [None] * self.channels
        self.clock = clock
        self.chip = chip
        self.tuning = tuning

    def _pitchstr(self, freq):
        return self.tuning.pitch(freq).str(self.mincents)

    def _effectstartstr(self, c):
        return self.wavestartstr if self.effects[c] is not self.chip.timers[c].effect.value else ''

    def _lhsvals(self, c):
        if self.chip.levelmodes[c].value or self.chip.fixedlevels[c].value:
            tone = self.chip.toneflags[c].value
            noise = self.chip.noiseflags[c].value
            if tone:
                toneperiod = self.chip.toneperiods[c].value
                yield toneperiod if self.periods else self._pitchstr(self.clock.tonefreq(toneperiod))
            else:
                yield ''
            yield '&' if tone and noise else ''
            yield self.chip.noiseperiod.value if noise else ''
            yield '*' if tone or noise else ''
        else:
            yield ''
            yield ''
            yield ''
            yield ''

    def _regularvals(self, c):
        yield from self._lhsvals(c)
        if self.chip.levelmodes[c].value:
            shape = self.chip.envshape.value
            envperiod = self.chip.envperiod.value
            yield self.shapes[shape]
            yield self.wavestartstr if self.shapeversion != self.chip.envshape.version else ''
            yield envperiod if self.periods else self._pitchstr(self.clock.envfreq(envperiod, shape))
        else:
            yield self.chip.fixedlevels[c].value or ''
            yield ''
            yield ''

    def _synthvals(self, c):
        yield from self._lhsvals(c)
        level = self.chip.fixedlevels[c].value
        if level:
            yield level
            yield self._effectstartstr(c)
            yield self._pitchstr(self.chip.timers[c].getfreq())
        else:
            yield ''
            yield '' # XXX: Indicate new effect even if we can't hear it?
            yield ''

    def _ddvals(self, c):
        yield ''
        yield ''
        yield ''
        yield ''
        yield '%%'
        yield self._effectstartstr(c)
        yield ''

    effecttypetovals = {
        DigiDrumEffect: _ddvals,
        type(NullEffect): _regularvals,
        type(PWMEffect): _synthvals,
        type(SinusEffect): _synthvals,
    }

    def _getvals(self, c):
        return self.effecttypetovals[type(self.chip.timers[c].effect.value)](self, c)

    def update(self):
        if self.line == self.height:
            self.stream.write(self.jump)
            self.line = 0
        print(self.format % tuple(v for c in range(self.channels) for v in self._getvals(c)), file = self.stream)
        self.shapeversion = self.chip.envshape.version
        for c in range(self.channels):
            self.effects[c] = self.chip.timers[c].effect.value
        self.line += 1 class LogicalRegisters:

    @types(Config, ClockInfo)
    def __init__(self, config, clockinfo, minnoiseperiod = 1):
        channels = range(config.chipchannels)
        # TODO: Make effective tone period user-readable without exposing mintoneperiod.
        # Clamping 0 to 1 is authentic for all 3 kinds of period, see qtonpzer, qnoispec, qenvpzer respectively:
        self.toneperiods = [Reg(maxval = config.maxtoneperiod, minval = clockinfo.mintoneperiod) for _ in channels]
        self.noiseperiod = Reg(maxval = config.maxnoiseperiod, minval = minnoiseperiod)
        self.toneflags = [Reg() for _ in channels]
        self.noiseflags = [Reg() for _ in channels]
        self.fixedlevels = [Reg(maxval = 0x0f, minval = 0) for _ in channels]
        self.levelmodes = [Reg() for _ in channels]
        self.envshape = VersionReg(maxval = 0x0f, minval = 0)
        self.envperiod = Reg(maxval = config.maxenvperiod, minval = 1)
        initialmixerflag = MixerFlag(0)(0) # Result is the same whatever the bit.
        for c in channels:
            self.toneperiods[c].value = TP(0, 0)
            self.toneflags[c].value = initialmixerflag
            self.noiseflags[c].value = initialmixerflag
            self.fixedlevels[c].value = 0
            self.levelmodes[c].value = getlevelmode(0)
        self.noiseperiod.value = 0
        self.envperiod.value = EP(0, 0)
        self.envshape.value = 0
        self.timers = tuple(MFPTimer() for _ in channels) class PhysicalRegisters:

    supportedchannels = 3
    timers = property(lambda self: self.logical.timers)
    levelbase = 0x8

    @types(Config, LogicalRegisters)
    def __init__(self, config, logical):
        # XXX: Add reverse wiring?
        # Like the real thing we have 16 registers, this impl ignores the last 2:
        self.R = tuple(Reg() for _ in range(16)) # Assume all incoming values are in [0, 255].
        # We only have registers for the authentic number of channels:
        for c in range(min(self.supportedchannels, config.chipchannels)):
            logical.toneperiods[c].link(TP, self.R[c * 2], self.R[c * 2 + 1])
            logical.toneflags[c].link(MixerFlag(c), self.R[0x7])
            logical.noiseflags[c].link(MixerFlag(self.supportedchannels + c), self.R[0x7])
            logical.fixedlevels[c].link(lambda l: l & 0x0f, self.R[self.levelbase + c])
            logical.levelmodes[c].link(getlevelmode, self.R[self.levelbase + c])
        logical.noiseperiod.link(NP, self.R[0x6])
        logical.envperiod.link(EP, self.R[0xB], self.R[0xC])
        logical.envshape.link(lambda s: s & 0x0f, self.R[0xD])
        for r in self.R:
            r.value = 0
        self.logical = logical class YM2149(Container):

    noiseshape = Shape(Lfsr(ym2149nzdegrees))

    @types(Config, ClockInfo, AmpScale, LogicalRegisters)
    def __init__(self, config, clockinfo, ampscale, logical):
        self.scale = clockinfo.scale
        channels = config.chipchannels
        self.oscpause = config.oscpause
        self.clock = clockinfo.implclock
        # Chip-wide signals:
        noise = NoiseOsc(self.scale, logical.noiseperiod, self.noiseshape)
        env = EnvOsc(self.scale, logical.envperiod, logical.envshape)
        # Digital channels from binary to level in [0, 31]:
        tones = [ToneOsc(self.scale, logical.toneperiods[c]) for c in range(channels)]
        rtones = [RToneOsc(mfpclock, self.clock, logical.timers[c], logical.fixedlevels[c]) for c in range(channels)]
        # XXX: Add rtones to maskables?
        self.maskables = tones + [noise, env] # Maskable by mixer and level mode.
        binchans = [BinMix(tones[c], noise, logical.toneflags[c], logical.noiseflags[c]) for c in range(channels)]
        levels = [Level(logical.levelmodes[c], logical.fixedlevels[c], env, binchans[c], rtones[c], logical.timers[c].effect) for c in range(channels)]
        super().__init__([Dac(level, ampscale.log2maxpeaktopeak, channels) for level in levels])

    def callimpl(self):
        result = super().callimpl()
        if not self.oscpause:
            # Pass the block to any nodes that were masked:
            for maskable in self.maskables:
                maskable(self.block, True) # The masked flag tells the node we don't care about output.
        return result class YMOpen(YMFile):

    impls = {i.formatid.encode(): i for i in [YM2, YM3, YM3b, YM5, YM6]}
    magic = b'YM'

    @types(Config)
    def __init__(self, config):
        self.path = config.inpath
        self.once = config.ignoreloop

    def start(self):
        self.startimpl()
        for info in self.ym.info:
            log.info(info)
        self.nominalclock = self.ym.clock
        self.pianorollheight = self.updaterate = self.ym.framefreq

    def startimpl(self):
        self.f = open(self.path, 'rb')
        try:
            if self.magic == self.f.read(2):
                self.ym = self.impls[self.magic + self.f.read(2)](self.f, self.once)
                return
        except:
            self.f.close()
            raise
        self.f.close()
        self.f = UnpackedFile(self.path)
        try:
            self.ym = self.impls[self.f.read(4)](self.f, self.once)
            # return
        except:
            self.f.close()
            raise

    def frames(self, chip):
        for frame in self.ym:
            frame.applydata(chip)
            yield

    def stop(self):
        self.f.close() class Player(MainBackground):

    @types(Config, Bundle, Roll, Timer, Stream)
    def __init__(self, config, bundle, roll, timer, stream):
        super().__init__(config)
        self.updaterate = config.updaterate
        self.bundle = bundle
        self.roll = roll
        self.timer = timer
        self.stream = stream

    def __call__(self):
        for _ in self.bundle:
            if self.quit:
                break
            self.roll.update()
            for b in self.timer.blocksforperiod(self.updaterate):
                self.stream.call(b)
        self.stream.flush() class LogicalBundle(Bundle):

    @types(Prerecorded, [Chip])
    def __init__(self, prerecorded, chips):
        super().__init__(prerecorded, {chip.param: chip.channels for chip in chips}) class PhysicalBundle(Bundle):

    @types(Prerecorded, PhysicalRegisters)
    def __init__(self, prerecorded, registers):
        super().__init__(prerecorded, registers)
from . import minblep, pitch
from .budgie import readbytecode
from .clock import ClockInfo
from .config import ConfigName
from .dosound import Bytecode
from .iface import Config
from .lurlene import loadcontext, LurleneBridge, YM2149Chip
from .out import StereoInfo, YMStream
from .timerimpl import ChipTimer, SimpleChipTimer, SyncTimer
from .util import initlogging, MainThread
from .vis import NullRoll, RollImpl
from .ym2149 import LogicalRegisters, PhysicalRegisters, YM2149
from .ymformat import YMOpen
from .ymplayer import Player, LogicalBundle, PhysicalBundle
from diapyr import DI, types
from diapyr.start import Started
from lurlene.context import Context
        from . import resid
    from . import jackclient
    from . import txt
    from . import out
    from . import out
    from . import jackclient
    from . import portaudioclient
    from . import txt
    from . import out
    from . import jackclient
    from . import portaudioclient
    from . import txt
    from . import out
import logging, lurlene.osc, sys
# Copyright 2014, 2018, 2019, 2020 Andrzej Cichocki

# This file is part of pym2149.
#
# 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/>.


log = logging.getLogger(__name__)

def boot(configname):
    di = DI()
    di.add(configname.loadconfig)
    di.add(di)
    config = di(Config)
    config_repr = list(config.repr)
    if config_repr:
        for key in config_repr:
            print(repr((-getattr(config, key)).scope().unravel()))
        sys.exit()
    di.add(ClockInfo)
    di.add(StereoInfo)
    di.add(YM2149)
    di.add(PhysicalRegisters)
    di.add(LogicalRegisters)
    di.add(minblep.loadorcreate)
    di.add(YMStream)
    pitch.configure(di)
    di.add(Context)
    di.add(RollImpl if config.pianorollenabled else NullRoll)
    di.add(MainThread)
    di.add(YM2149Chip)
    if config.SID.enabled:
        resid.configure(di)
    return config, di

def main_bpmtool():
    'Show a table of speed (updates per tracker line) to BPM.'
    config, _ = boot(ConfigName())
    ups = config.updaterate
    lpb = config.linesperbeat
    for upl in range(1, 21):
        lpm = 60 * ups / upl
        bpm = lpm / lpb
        print(f"{upl:2} {bpm:7.3f}")

@types(Config, this = Bytecode)
def srcbytecodefactory(config):
    with open(config.inpath) as f:
        return Bytecode(readbytecode(f, config.srclabel), config.dosoundextraseconds)

def main_dosound2jack():
    'Play a Dosound script via JACK.'
    initlogging()
    config, di = boot(ConfigName('inpath', 'srclabel'))
    with di:
        di.add(srcbytecodefactory)
        jackclient.configure(di)

        di.add(SyncTimer)