Module musx.score
This module implements a scheduling queue for generating musical scores. The Score runs 'part composers' (Python generators) that output musical events to sequences and score files.
Example
seq = Seq()
sco = Score(out=seq)
def bach(score, num):
    for i in range(num):
        n = Note(time=score.now, key=60+i)
        score.add(n)
        yield .125
sco.compose(bach(sco, 10))
See the demos directory for many examples of part composers!
Expand source code
###############################################################################
"""
This module implements a scheduling queue for generating musical scores.
The Score runs 'part composers' (Python generators) that output musical events
to sequences and score files.
Example
-------
```py
seq = Seq()
sco = Score(out=seq)
def bach(score, num):
    for i in range(num):
        n = Note(time=score.now, key=60+i)
        score.add(n)
        yield .125
sco.compose(bach(sco, 10))
```
See the demos directory for many examples of part composers!
"""
import types
class Score:
    """
    Runs part composers in time sorted order. A part composer is a Python generator
    that is evaluated by the Score. When a part composer executes it
    can add musical events or new part composers to the score. To continue
    running a part composer must yield back a delta time that specifies the next time
    point at which the composer should run again. A part composer stops 
    running when it stops yielding delta times or it yields a negative delta time.
    """
    _queue = []
    """
    A time sorted list of queue entries. A queue entry is a list: 
    [<runtime>, <starttime>, <composer>], where <runtime> is the next time 
    (in seconds) at which <composer> will be called, <starttime> is 
    the time at which <composer> was initially inserted into the scheduer,
    and <composer> is a generator.
    """
    _latest_time = 0
    """The run time of the last queue entry."""
    def __init__(self, out=None):
        self._clean()
        self.out = out
    def _clean(self):
        self._queue = []
        self.running = False
        self._latest_time = self.now = self.elapsed = 0
    def _insert(self, entry):
        """
        Insert entry after all entries <= to it. Optimized
        for the most common case of appending at the end.
        """
        if entry[0] < self._latest_time:
            for i, e in enumerate(self._queue):
                if e[0] > entry[0]:
                    #print("inserting", entry)
                    self._queue.insert(i, entry)
                    return
        #print("insert: appending", entry)
        self._queue.append(entry)
        self._latest_time = entry[0]
    def _insure_entries(self, expr):
        """
        Parses composer expression(s) into a list of one or
        more queue entries. A composer expression can be a 
        composer, a list [*ahead*, *composer*], or a sequence
        of the same: [[*ahead1*, *composer1*], ...]
        """
        def isgen(x): return isinstance(x, types.GeneratorType)
        def isnum(x): return isinstance(x, (int, float))
        def islist(x): return isinstance(x, list)
        def ispair(x):
            if len(x) == 0:
                raise ValueError("empty generator spec.")
            elif isnum(x[0]):
                if x[0] < 0:
                    raise ValueError(f"not a number >= 0: {x}.")
                if len(x) != 2:
                    raise ValueError(f"not a generator spec: {x}.")
                if not isgen(x[1]):
                    raise ValueError(f"not a generator spec: {x}.")
                return True
            return False      
        
        now = self.now
        if not islist(expr):
            if isgen(expr):
                return [[now + 0, now + 0, expr]]
            raise TypeError(f"not a generator: {expr}")
        elif ispair(expr):
            return [[now + expr[0], now + expr[0], expr[1]]]
        # If we reach here, expr is a list that does not start with
        # a number so every item has to be a gen or a list [num gen]
        entries = []
        for e in expr:
            #print("e:", e)
            if not islist(e):
                if isgen(e):
                    entries.append([now + 0, now + 0, e])
                else:
                    raise TypeError(f"not a generator: {e}.")
            elif ispair(e):
                entries.append([now + e[0], now + e[0], e[1]])
            else:
                raise ValueError(f"not a generator list [time, gen]: {e}.")
        return entries
    def _run(self):
        """
        The scheduling loop that processes queued composers until there are
        no more composers to run. To process the current composer in the
        queue the Score performs the following tasks:
        * the current (first) composer is popped off the queue.
        * the queue's self.now and self.elapsed times are updated.
        * the composer generator is called via next().
        * if the next() call yields a positive ahead value back the composer
          is reinserted  back into the queue at time now + ahead, otherwise
          the composer is not reinserted in the queue.
        """
        self.running = True
        while len(self._queue) > 0:
            # Pop earliest entry from the queue and get its composer
            entry = self._queue.pop(0)
            # Set the current score time to the entry's time.
            self.now = entry[0]
            # print(f"before composer, score time is {self.now}")
            # Set the composer's elapsed time since starting
            self.elapsed = self.now - entry[1]
            #print("***elapsed=", self.elapsed)
            composer = entry[2]
            try:
                # Call the entry's composer. if it yields a positive number
                # increment entry's current scoretime and add it back into the _queue.
                delta = next(composer)
                if isinstance(delta, (int, float)): # and delta >= 0:
                    if delta >= 0:
                        # Advance entry's time and insert back into queue
                        entry[0] += delta
                        # print(f"after composer, delta is {delta} and entry time is now {entry[0]}")
                        self._insert(entry)
                else:
                    raise ValueError(f"invalid yield value {delta} from composer {entry[2]}.")
            except StopIteration:
                # print("stopping", composer)
                pass
 
    out = None
    """
    An optional object a composer can add musical events to. The Score
    does not examine the contents this variable.
    """
    running = False
    """True if the Score is running."""
    now = 0
    """
    The time point in the score that the currently executing composer is at.
    """
 
    elapsed = 0
    """
    The amount of time the currently executing composer has been running since
    it was added to the Score.
    """
 
    def compose(self, composer):
        """
        Starts running one or more part composers. This function can be
        called at the top level and also by composers that are running.
        Parameters
        ----------
        composer : generator | list
            If composer is a generator it is added to the queue at the current
            time. Otherwise composer can be a list [*ahead*, *composer*]
            or a list of the same [[*ahead*, *composer*], ...]  where
            *ahead* is a future start time and *composer* is a part composer.
        Example
        -------
        ```py
        def simp(q, num, rate, key):
            for _ in range(num):
                print("simp at", q.now, "plays", key)
                yield rate
        q=Score()
        # running one composer
        q.compose(simp(q, 5, 1, 60))
        # running two at times 0 and 5
        q.compose([[0, simp(q, 5, 1, 60)], [5, simp(q, 10, .251, 72)]])
        # a composer that composes composers =:)
        def simp2(q, num)
            for i in range(num):
                print("simp2 at", q.now)
                q.compose([q.now+i*2, simp(num, 5, .25, 60+i)])
            yield 2
        q.compose(simp2(q, num))
        ```
        """
        #print("in compose")
        if self.running:
            # A composer added while running just gets inserted it at
            # the propper time in the queue which is already running.
            for e in self._insure_entries(composer):
                #print("r adding", e)
                self._insert(e)
        else:
            for e in self._insure_entries(composer):
                #print("x adding", e)
                self._insert(e)
            try:
                # This is a loop that processes the queue until it is empty. It
                # is in a try statement because we always want to clean up, even
                # if an error is thrown.
                self._run()
            finally:
                self._clean()
                #print("Done!")
    def add(self, event):
        """
        Adds an event to the score. 
        """
        self.out.add(event)
        # Since the score is a scheduler we can just append the event to the seq.
        #self.out.append(event)
if __name__ == '__main__':
    print('Score Tests...')
    def gen(): yield 0
    def foo(): return 0
    x = []
    q = Score()
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:",e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = gen
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = gen()
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = [5, gen()]
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = [gen(), 5]
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = [gen()]
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = [[gen()]]
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = [gen(), gen(), gen(), [20, gen]]
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = [gen(), gen(), gen(), [20, gen()]]
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = [5]
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = [0, gen(), 45]
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = [gen(), gen(), [45, gen()]]
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    x = [gen(), gen(), [-2, gen()]]
    print("\ntesting input:", x)
    try: l = q._insure_entries(x)
    except ValueError as e: print("ERROR:", e)
    except TypeError as e: print("ERROR:", e)
    else: print(l)
    print('...Done!')Classes
- class Score (out=None)
- 
Runs part composers in time sorted order. A part composer is a Python generator that is evaluated by the Score. When a part composer executes it can add musical events or new part composers to the score. To continue running a part composer must yield back a delta time that specifies the next time point at which the composer should run again. A part composer stops running when it stops yielding delta times or it yields a negative delta time. Expand source codeclass Score: """ Runs part composers in time sorted order. A part composer is a Python generator that is evaluated by the Score. When a part composer executes it can add musical events or new part composers to the score. To continue running a part composer must yield back a delta time that specifies the next time point at which the composer should run again. A part composer stops running when it stops yielding delta times or it yields a negative delta time. """ _queue = [] """ A time sorted list of queue entries. A queue entry is a list: [<runtime>, <starttime>, <composer>], where <runtime> is the next time (in seconds) at which <composer> will be called, <starttime> is the time at which <composer> was initially inserted into the scheduer, and <composer> is a generator. """ _latest_time = 0 """The run time of the last queue entry.""" def __init__(self, out=None): self._clean() self.out = out def _clean(self): self._queue = [] self.running = False self._latest_time = self.now = self.elapsed = 0 def _insert(self, entry): """ Insert entry after all entries <= to it. Optimized for the most common case of appending at the end. """ if entry[0] < self._latest_time: for i, e in enumerate(self._queue): if e[0] > entry[0]: #print("inserting", entry) self._queue.insert(i, entry) return #print("insert: appending", entry) self._queue.append(entry) self._latest_time = entry[0] def _insure_entries(self, expr): """ Parses composer expression(s) into a list of one or more queue entries. A composer expression can be a composer, a list [*ahead*, *composer*], or a sequence of the same: [[*ahead1*, *composer1*], ...] """ def isgen(x): return isinstance(x, types.GeneratorType) def isnum(x): return isinstance(x, (int, float)) def islist(x): return isinstance(x, list) def ispair(x): if len(x) == 0: raise ValueError("empty generator spec.") elif isnum(x[0]): if x[0] < 0: raise ValueError(f"not a number >= 0: {x}.") if len(x) != 2: raise ValueError(f"not a generator spec: {x}.") if not isgen(x[1]): raise ValueError(f"not a generator spec: {x}.") return True return False now = self.now if not islist(expr): if isgen(expr): return [[now + 0, now + 0, expr]] raise TypeError(f"not a generator: {expr}") elif ispair(expr): return [[now + expr[0], now + expr[0], expr[1]]] # If we reach here, expr is a list that does not start with # a number so every item has to be a gen or a list [num gen] entries = [] for e in expr: #print("e:", e) if not islist(e): if isgen(e): entries.append([now + 0, now + 0, e]) else: raise TypeError(f"not a generator: {e}.") elif ispair(e): entries.append([now + e[0], now + e[0], e[1]]) else: raise ValueError(f"not a generator list [time, gen]: {e}.") return entries def _run(self): """ The scheduling loop that processes queued composers until there are no more composers to run. To process the current composer in the queue the Score performs the following tasks: * the current (first) composer is popped off the queue. * the queue's self.now and self.elapsed times are updated. * the composer generator is called via next(). * if the next() call yields a positive ahead value back the composer is reinserted back into the queue at time now + ahead, otherwise the composer is not reinserted in the queue. """ self.running = True while len(self._queue) > 0: # Pop earliest entry from the queue and get its composer entry = self._queue.pop(0) # Set the current score time to the entry's time. self.now = entry[0] # print(f"before composer, score time is {self.now}") # Set the composer's elapsed time since starting self.elapsed = self.now - entry[1] #print("***elapsed=", self.elapsed) composer = entry[2] try: # Call the entry's composer. if it yields a positive number # increment entry's current scoretime and add it back into the _queue. delta = next(composer) if isinstance(delta, (int, float)): # and delta >= 0: if delta >= 0: # Advance entry's time and insert back into queue entry[0] += delta # print(f"after composer, delta is {delta} and entry time is now {entry[0]}") self._insert(entry) else: raise ValueError(f"invalid yield value {delta} from composer {entry[2]}.") except StopIteration: # print("stopping", composer) pass out = None """ An optional object a composer can add musical events to. The Score does not examine the contents this variable. """ running = False """True if the Score is running.""" now = 0 """ The time point in the score that the currently executing composer is at. """ elapsed = 0 """ The amount of time the currently executing composer has been running since it was added to the Score. """ def compose(self, composer): """ Starts running one or more part composers. This function can be called at the top level and also by composers that are running. Parameters ---------- composer : generator | list If composer is a generator it is added to the queue at the current time. Otherwise composer can be a list [*ahead*, *composer*] or a list of the same [[*ahead*, *composer*], ...] where *ahead* is a future start time and *composer* is a part composer. Example ------- ```py def simp(q, num, rate, key): for _ in range(num): print("simp at", q.now, "plays", key) yield rate q=Score() # running one composer q.compose(simp(q, 5, 1, 60)) # running two at times 0 and 5 q.compose([[0, simp(q, 5, 1, 60)], [5, simp(q, 10, .251, 72)]]) # a composer that composes composers =:) def simp2(q, num) for i in range(num): print("simp2 at", q.now) q.compose([q.now+i*2, simp(num, 5, .25, 60+i)]) yield 2 q.compose(simp2(q, num)) ``` """ #print("in compose") if self.running: # A composer added while running just gets inserted it at # the propper time in the queue which is already running. for e in self._insure_entries(composer): #print("r adding", e) self._insert(e) else: for e in self._insure_entries(composer): #print("x adding", e) self._insert(e) try: # This is a loop that processes the queue until it is empty. It # is in a try statement because we always want to clean up, even # if an error is thrown. self._run() finally: self._clean() #print("Done!") def add(self, event): """ Adds an event to the score. """ self.out.add(event) # Since the score is a scheduler we can just append the event to the seq. #self.out.append(event)Class variables- var elapsed
- 
The amount of time the currently executing composer has been running since it was added to the Score. 
- var now
- 
The time point in the score that the currently executing composer is at. 
- var out
- 
An optional object a composer can add musical events to. The Score does not examine the contents this variable. 
- var running
- 
True if the Score is running. 
 Methods- def add(self, event)
- 
Adds an event to the score. Expand source codedef add(self, event): """ Adds an event to the score. """ self.out.add(event) # Since the score is a scheduler we can just append the event to the seq. #self.out.append(event)
- def compose(self, composer)
- 
Starts running one or more part composers. This function can be called at the top level and also by composers that are running. Parameters- composer:- generator | list
- If composer is a generator it is added to the queue at the current time. Otherwise composer can be a list [ahead, composer] or a list of the same [[ahead, composer], …] where ahead is a future start time and composer is a part composer.
 Exampledef simp(q, num, rate, key): for _ in range(num): print("simp at", q.now, "plays", key) yield rate q=Score() # running one composer q.compose(simp(q, 5, 1, 60)) # running two at times 0 and 5 q.compose([[0, simp(q, 5, 1, 60)], [5, simp(q, 10, .251, 72)]]) # a composer that composes composers =:) def simp2(q, num) for i in range(num): print("simp2 at", q.now) q.compose([q.now+i*2, simp(num, 5, .25, 60+i)]) yield 2 q.compose(simp2(q, num))Expand source codedef compose(self, composer): """ Starts running one or more part composers. This function can be called at the top level and also by composers that are running. Parameters ---------- composer : generator | list If composer is a generator it is added to the queue at the current time. Otherwise composer can be a list [*ahead*, *composer*] or a list of the same [[*ahead*, *composer*], ...] where *ahead* is a future start time and *composer* is a part composer. Example ------- ```py def simp(q, num, rate, key): for _ in range(num): print("simp at", q.now, "plays", key) yield rate q=Score() # running one composer q.compose(simp(q, 5, 1, 60)) # running two at times 0 and 5 q.compose([[0, simp(q, 5, 1, 60)], [5, simp(q, 10, .251, 72)]]) # a composer that composes composers =:) def simp2(q, num) for i in range(num): print("simp2 at", q.now) q.compose([q.now+i*2, simp(num, 5, .25, 60+i)]) yield 2 q.compose(simp2(q, num)) ``` """ #print("in compose") if self.running: # A composer added while running just gets inserted it at # the propper time in the queue which is already running. for e in self._insure_entries(composer): #print("r adding", e) self._insert(e) else: for e in self._insure_entries(composer): #print("x adding", e) self._insert(e) try: # This is a loop that processes the queue until it is empty. It # is in a try statement because we always want to clean up, even # if an error is thrown. self._run() finally: self._clean() #print("Done!")