Python+PyAudio+MMLでスーパーマリオの地上BGMを

MML http://ja.wikipedia.org/wiki/Music_Macro_Language

PyAudioで何か作ってみようと思って書いた。
簡単な機能しかついていないし、汎用性に欠ける。
音もゴミ収集車みたいだし

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import re
import math
import array
import itertools
import collections

import pyaudio


class MMLStatus:
    """テンポとか音符の長さとかの情報を持つ"""
    def __init__(self, T=120, L=4, O=4, Q=5, V=12):
        self.T = T
        self.L = L
        self.O = O
        self.Q = Q
        self.V = V
        
    def __str__(self):
        return 'T=%d, L=%d, O=%d, Q=%d, V=%d' % (self.T, self.L, self.O, self.Q, self.V)
    
    def change(self, cmd):
        if cmd.cmd == '<':
            if self.O < 7: self.O += 1
        elif cmd.cmd == '>':
            if self.O > 0: self.O -= 1
        elif cmd.cmd == 'L':
            if 1 <= cmd.arg <= 64: self.L = cmd.arg
        elif cmd.cmd == 'V':
            if 0 <= cmd.arg <= 16: self.V = cmd.arg
        elif cmd.cmd == 'T':
            if 30 <= cmd.arg <= 240: self.T = cmd.arg
        elif cmd.cmd == 'O':
            if 0 <= cmd.arg <= 7: self.O = cmd.arg
        elif cmd.cmd == 'Q':
            if 1 <= self.Q <= 8: self.Q = cmd.arg - 1



MMLCommand = collections.namedtuple('MMLCommand', 'cmd acci arg dot')
ADSR = collections.namedtuple('ADSR', 'A D S R')
class MMLPlayer:
    """MMLプレイヤー"""
    ACCI = ( 1.0, 1.0/1.06, 1.06, 1.06 )
    DOT  = ( 1.0, 1.5, 1.75, 1.875 )
    
    def __init__(self, tone, adsr=None):
        self.tone = tone
        self.adsr = adsr
        self.stat = MMLStatus()
        
        
    def fetch_token(self, command):
        buf = []
        for c in command.upper():
            if c in (' ', '\n'):
                continue
            if c in ('A', 'B', 'C', 'D', 'E', 'F', 'G',
                     'R', 'T', 'L', 'O', 'V', 'Q', '>', '<'):
                if buf:
                    yield ''.join(buf)
                    buf = []
            buf.append(c)
        else:
            if buf: yield ''.join(buf)
    
    
    def conv_token(self, token):
        m = re.search(r'([A-Z<>])([-=+#]?)(\d*)(\.*)', token)
        if not m: return
        cmd  = m.group(1).upper()
        acci = '=-+#'.index(m.group(2))
        arg  = int(m.group(3) or 0)
        dot  = len(m.group(4))
        return MMLCommand(cmd=cmd, acci=acci, arg=arg, dot=dot)
    
    
    def conv_freq(self, cmd):
        freq = 0
        if cmd.cmd == 'C':
            freq = 261.63
        elif cmd.cmd == 'D':
            freq = 293.66
        elif cmd.cmd == 'E':
            freq = 329.63
        elif cmd.cmd == 'F':
            freq = 349.23
        elif cmd.cmd == 'G':
            freq = 392.00
        elif cmd.cmd == 'A':
            freq = 440.00
        elif cmd.cmd == 'B':
            freq = 493.88
        elif cmd.cmd == 'R':
            freq = 0
        freq *= 2 ** (self.stat.O - 4) * self.ACCI[cmd.acci]
        return freq
    
    
    def conv_length(self, cmd):
        if 1 <= cmd.arg <= 64:
            return cmd.arg
        else:
            return self.stat.L
        
        
    def calc_tim(self, cmd):
        return (60.0 / self.stat.T) * (4.0 / self.conv_length(cmd)) * self.DOT[cmd.dot]
    
    
    def play(self, command):
        p = pyaudio.PyAudio()
        stream = p.open(rate=44100, channels=1, format=pyaudio.paFloat32, output=True)
        
        buf = (0, 0, 0, 0)
        for token in self.fetch_token(command):
            cmd = self.conv_token(token)
            if cmd.cmd not in ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'R'):
                self.stat.change(cmd)
                continue
            
            freq = self.conv_freq(cmd)
            tim  = self.calc_tim(cmd)
            tim2 = tim * ((self.stat.Q + 1) / 8.0)
            velocity = self.stat.V / 16.0 * 0.1

            print cmd, self.stat
            
            if cmd.cmd != 'R':
                stream.write(self.tone(buf[0], sec=(buf[1], buf[2]), velocity=velocity, adsr=self.adsr))
                buf = (freq, tim, tim2, velocity)
            else:
                buf = (buf[0], buf[1] + tim, buf[2], velocity)
        else:
            stream.write(self.tone(buf[0], sec=(buf[1], buf[2]), velocity=velocity, adsr=self.adsr))
            
        stream.close()
        p.terminate()



def sine_wave(w, n):
    """サイン派"""
    for i in xrange():
        yield math.sin(float(i % w) / w * PI2)

def sawtooth_wave(w, n):
    """ノコギリ波"""
    for i in xrange(n):
        yield (i % w) / float(w)

def square_wave(w, n):
    """矩形波"""
    hw = w / 2
    for i in xrange(n):
        yield 1.0 if i % w <= hw else 0.0


def tone(freq, sec=1, velocity=.2, rate=44100, adsr=None):
    PI2 = math.pi * 2
    
    if isinstance(sec, (int, float)):
        tim = sec
    else:
        sec, tim = sec
    
    w = rate / freq if freq else 0
    
    def envelope_generator():
        if adsr:
            length = rate * sec
            
            i = 0
            attack  = rate * adsr.A
            for i in xrange(int(min(attack, length))):
                yield i / float(attack)
            
            decay   = rate * adsr.D    
            sustain = adsr.S    
            for i in xrange(i, int(min(decay, length))):
                j = i - attack
                yield 1.0 - ((1.0 - sustain) * j / float(decay))
                
            release_start = rate * tim
            for i in xrange(i, int(min(release_start, length))):
                yield sustain
            
            release = rate * adsr.R
            for i in xrange(i, int(min(release, length))):
                j = i - release_start
                yield sustain * (1.0 - (j / float(release)))
                
            for i in xrange(i, int(length)):
                yield 0.0
                
        else:
            for i in xrange(int(rate * sec)):
                yield 1.0
    
    wgen = square_wave
    def gen():
        if w:
            for i, j in itertools.izip(wgen(w, int(rate * sec)), envelope_generator()):
                yield i * j * velocity
        else:
            for i in xrange(int(rate * sec)):
                yield 0
                
    return array.array('f', gen()).tostring()

    


# http://blog.lilyx.net/2007/10/07/javascript-mml-mario-theme/

command = """
T195 L8 O5
eerercergrr4>grr4<

crr>grrerrarbrb-arg6<c6g6arfgrercd>br4<
crr>grrerrarbrb-arg6<c6g6arfgrercd>br4<

r4gf+fd+rer>g+a<c>ra<cd
r4gf+fd+rer<crccrr4
>r4gf+fd+rer>g+a<c>ra<cd
r4e-rrdrrcrr4r2

r4gf+fd+rer>g+a<c>ra<cd
r4gf+fd+rer<crccrr4
>r4gf+fd+rer>g+a<c>ra<cd
r4e-rrdrrcrr4r2

ccrcrcdrecr>agrr4
<ccrcrcder1
ccrcrcdrecr>agrr4<

eerercergrr4>grr4<
crr>grrerrarbrb-arg6<c6g6arfgrercd>br4<
crr>grrerrarbrb-arg6<c6g6arfgrercd>br4<

ecr>gr4g+ra<frf>arr4
b6<a6a6a6g6f6ecr>agrr4<
ecr>gr4g+ra<frf>arr4
b6<f6f6f6e6d6crr2.

ecr>gr4g+ra<frf>arr4
b6<a6a6a6g6f6ecr>agrr4<
ecr>gr4g+ra<frf>arr4
b6<f6f6f6e6d6crr2.

ccrcrcdrecr>agrr4
<ccrcrcder1
ccrcrcdrecr>agrr4<

eerercergrr4>grr4<

ecr>gr4g+ra<frf>arr4
b6<a6a6a6g6f6ecr>agrr4<
ecr>gr4g+ra<frf>arr4
b6<f6f6f6e6d6crr2.

T180 c4.>g4.e4 T160 a6b6a6 T150 a-6b-6a-6g1
"""

MMLPlayer(tone, adsr=ADSR(0., .4, 0., 0.2)).play(command)