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)