-
-
Save snipsnipsnip/1364375 to your computer and use it in GitHub Desktop.
ruby に移植 (第1回 Scheme コードバトンの成果のsynthesizer)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# https://gist.github.com/297312 から | |
# rubyで遊んでみたかったので勝手に移植させていただきました | |
module Synthesizer | |
module_function | |
# RIFFフォーマットのwavデータを生成します。 | |
# sampling_rate: サンプリングレート | |
# wave_data: 波形データ。振幅の値(-1から1の値)のリストになります。 | |
# 戻り値: wavデータの不完全文字列 | |
# ※手抜きしているので、リトルエンディアンの環境でしか動作しません。 | |
def wave_to_riff(sampling_rate, wave_data) | |
header = [ | |
"RIFF", | |
36 + wave_data.size * 2, | |
"WAVE", | |
"fmt ", | |
16, | |
1, | |
1, | |
sampling_rate, | |
sampling_rate * 2, | |
2, | |
16, | |
"data", | |
wave_data.size * 2, | |
] | |
# 移植ついでに正規化 | |
min, max = wave_data.minmax | |
f = 2.0 / (max - min) | |
pcm_data = wave_data.map {|w,i| (((w - min) * f - 1) * 32767.0).round } | |
riff_data = header + pcm_data | |
riff_data.pack("A4VA4A4lvvllvvA4ls*") | |
end | |
# 波形データを生成します。 | |
# sampling_rate: サンプリングレート | |
# wave_form: -1から1の要素を持つベクトル。矩形波だと#(1 -1)のようになります。 | |
# freq: 周波数 | |
# sec: 秒数 | |
# 戻り値: 波形データ | |
def oscillator(sampling_rate, wave_form, freq, sec) | |
len = (sampling_rate * sec).to_i | |
return Array.new(len, 0) unless freq | |
p freq | |
size = wave_form.size - 1 | |
f = freq.to_f * sec / len | |
Array.new(len) do |i| | |
l = i * f | |
wave_form[(size * (l % 1)).round] | |
end | |
end | |
# 音階を与えて、波形データを生成します。 | |
# sampling_rate: サンプリングレート | |
# pitch: ピッチ。O4のドを60とした数値。1オクターブ12音なので、O5のドだと72になります。 | |
# sec: 秒数 | |
# キーワード引数として、:wave_form 波形データ をとります。 | |
# (移植でオプション引数に変更) | |
# 戻り値: 波形データ | |
def pitch(sampling_rate, pitch, sec, wave_form=[1, -1]) | |
pitch and freq = 440 * 2 ** ((pitch - 69) / 12.0) | |
oscillator(sampling_rate, wave_form, freq, sec) | |
end | |
# エンベロープのパラメータを与えて、波形データを変化させます。 | |
# env_param: ベクトルで、アタックタイム、ディレイタイム、サステインレベル、リリースタイムを順に含みます。1で正規化しておいてください。 | |
# wave_data: 入力の波形データ | |
# 戻り値: 変化した波形データ | |
def envelope(env_param, wave_data) | |
len = wave_data.size.to_f | |
ta = len * env_param[0] | |
td = len * env_param[1] | |
ls = env_param[2] | |
tr = len * env_param[3] | |
filters = [ | |
[1 / ta, 0, ta], | |
[(ls - 1) / td, 1 + ta * (1 - ls) / td, ta + td], | |
[0, ls, len - tr], | |
[-ls / tr, len * ls / tr, len], | |
] | |
Array.new(wave_data.size) do |i| | |
w = wave_data[i] | |
filters.each do |a, b, limit| | |
if i < limit | |
w *= a * i + b | |
break | |
end | |
end | |
w | |
end | |
end | |
# 複数の波形データを合成します。和音を作るときに使用します。 | |
# wave_data_list: 波形データのリスト | |
# 戻り値: 合成された波形データ | |
def merge_wave(wave_data_list) | |
size = wave_data_list.map {|w| w.size }.max | |
Array.new(size) do |i| | |
s = c = 0 | |
wave_data_list.each do |w| | |
if a = w[i] | |
s += a | |
c += 1 | |
end | |
end | |
c == 0 ? 0 : s / c | |
end | |
end | |
# 複数の波形データを連結します。 | |
# wave_data_list: 波形データのリスト | |
# 戻り値: 連結された波形データ | |
def concat_wave(wave_data_lst) | |
wave_data_lst.flatten | |
end | |
# MML(Music Macro Language)から、波形データを生成します。 | |
# sampling_rate: サンプリングレート | |
# expr: 昔のN88_BASIC風のMMLです。[:c, 4] で4分音符のドになります。[:o, 4] でオクターブの指定、[:r, 4]で休符になります。これらをリストで与えてください。 | |
# キーワード引数として、:tempo テンポ、:wave_form 波形データ、:envelope エンベロープパラメータ をとります。 | |
# 戻り値: 波形データ | |
def mml_to_wave(sampling_rate, expr, opts={}) | |
tempo = opts[:tempo] || 120 | |
wave_form = opts[:wave_form] || [1, -1] | |
env = opts[:envelope] | |
octave = 5 | |
wav = expr.map do |a, b| | |
case a | |
when :r | |
length = b | |
pitch(sampling_rate, nil, l_to_sec(tempo, length), nil) | |
when :o | |
octave = b + 1 | |
nil | |
else | |
note = a | |
length = b | |
pitch = 12 * octave + Notes[note] | |
wav = pitch(sampling_rate, pitch, l_to_sec(tempo, length), wave_form) | |
wav = envelope(env, wav) if env | |
wav | |
end | |
end | |
wav.flatten! | |
wav.compact! | |
wav | |
end | |
Notes = {} | |
[ | |
[:c], | |
[:"c+", :"d-"], | |
[:d], | |
[:"d+", :"e-"], | |
[:e], | |
[:f], | |
[:"f+", :"g-"], | |
[:g], | |
[:"g+", :"a-"], | |
[:a], | |
[:"a+", :"b-"], | |
[:b] | |
].each_with_index do |names,i| | |
names.each {|n| Notes[n] = i } | |
end | |
def l_to_sec(tempo, l) | |
s = 240.0 / (tempo * l) | |
p s | |
s | |
end | |
# MML(Music Macro Language)から、wavデータを生成します。MMLは複数指定でき、それらは合成されるので、和音も出せます。 | |
# mml_list: MMLのリスト。 | |
# キーワード引数として、:tempo テンポ、:wave_form 波形データ、:envelope エンベロープパラメータ、:sampling_rate サンプリングレート をとります。 | |
# 戻り値: wavデータの不完全文字列 | |
def mml_to_riff(mml_list, opts={}) | |
sampling_rate = opts[:sampling_rate] || 44100 | |
tracks = mml_list.map {|mml| mml_to_wave(sampling_rate, mml, opts) } | |
wav = merge_wave(tracks) | |
wave_to_riff(sampling_rate, wav) | |
end | |
end | |
if $0 == __FILE__ | |
yes = Synthesizer.mml_to_riff([ | |
[[:o, 6], [:e, 8], [:c, 8], [:e, 8], [:c, 2]] | |
], { | |
:envelope => [0, 0.1, 0.5, 0.6], | |
:wave_form => [0, 0.5, 1, -1, -0.5] | |
}) | |
no = Synthesizer.mml_to_riff([ | |
[[:o, 2], [:c, 8], [:c, 2]] | |
], { | |
:tempo => 240, | |
:envelope => [0, 0, 0.1, 0.05] | |
}) | |
open("yes.wav", "wb") {|f| f << yes } | |
open("no.wav", "wb") {|f| f << no } | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ベタ移植から自分の好みにあわせて整理した版 | |
require 'strscan' | |
require 'rational' | |
class Wave | |
include Enumerable | |
attr_reader :sampling_rate, :wave | |
alias to_a wave | |
def self.add(wave, *waves) | |
waves.unshift wave | |
longest = waves.max_by {|w| w.size } | |
waves.delete_at waves.index(longest) | |
longest.dup.add!(*waves) | |
end | |
def self.add!(wave, *waves) | |
waves.unshift wave | |
longest = waves.max_by {|w| w.size } | |
waves.delete_at waves.index(longest) | |
longest.add!(*waves) | |
end | |
def self.concat(wave, *waves) | |
wave.dup.concat!(*waves) | |
end | |
def self.concat!(wave, *waves) | |
wave.concat!(*waves) | |
end | |
def initialize(sampling_rate, wave) | |
@sampling_rate = sampling_rate | |
@wave = wave | |
end | |
def initialize_copy(orig) | |
@wave = @wave.dup | |
end | |
def inspect | |
"#<Wave:#{'%#x' % object_id} @sampling_rate=#{sampling_rate},@size=#{size},@wave=[#{@wave[0..9].join(',')}..]>" | |
end | |
def ==(wave) | |
@sampling_rate == wave.sampling_rate and | |
@wave == wave.wave | |
end | |
def size | |
@wave.size | |
end | |
def [](i) | |
@wave[i] | |
end | |
def each(&blk) | |
@wave.each(&blk) | |
end | |
def map!(&blk) | |
@wave.map!(&blk) | |
self | |
end | |
alias filter! map! | |
def map_with_index!(&blk) | |
@wave.enum_for(:map!).with_index(&blk) | |
self | |
end | |
alias filter_with_index! map_with_index! | |
def concat!(*waves) | |
return self if waves.empty? | |
unless waves.all? {|w| w.sampling_rate == sampling_rate } | |
raise ArgumentError, "sampling rate doesn't match" | |
end | |
waves.each {|w| @wave.concat(w.wave) } | |
self | |
end | |
def add!(*waves) | |
return self if waves.empty? | |
unless waves.all? {|w| w.sampling_rate == sampling_rate } | |
raise ArgumentError, "sampling rate doesn't match" | |
end | |
filter_with_index! do |w,i| | |
waves.each {|wave| a = wave[i] and w += a } | |
w | |
end | |
end | |
def normalize! | |
min, max = wave.minmax | |
f = 2.0 / (max - min) | |
filter! {|w| (w - min) * f - 1 } | |
end | |
def to_riff | |
header = [ | |
"RIFF", | |
36 + size * 2, | |
"WAVE", | |
"fmt ", | |
16, | |
1, | |
1, | |
sampling_rate, | |
sampling_rate * 2, | |
2, | |
16, | |
"data", | |
size * 2, | |
] | |
riff = header.concat(@wave.map {|w| (w * 32767.0).round }) | |
riff.pack("A4VA4A4lvvllvvA4ls*") | |
end | |
end | |
class Oscillator | |
attr_accessor :sampling_rate, :waveform | |
def initialize(sampling_rate=44100, waveform=[1, -1]) | |
@sampling_rate = sampling_rate | |
@orig_waveform = @waveform = waveform | |
end | |
def call(seconds, freq) | |
len = (seconds * sampling_rate).to_i | |
if freq == 0 | |
pcm = Array.new(len, @waveform[0]) | |
else | |
size = @waveform.size - 1 | |
f = freq.to_f * seconds / len | |
pcm = Array.new(len) {|i| @waveform[(size * ((i * f) % 1)).round] } | |
end | |
Wave.new(sampling_rate, pcm) | |
end | |
def notify(msg, *args) | |
case msg | |
when :waveform | |
@waveform = args | |
true | |
when :init | |
@waveform = @orig_waveform | |
else | |
false | |
end | |
end | |
end | |
class Envelope | |
attr_accessor :oscillator, :a, :d, :s, :r | |
def initialize(oscillator, a=0, d=0, s=1, r=0) | |
@oscillator = oscillator | |
@orig = [a, d, s, r] | |
set(a, d, s, r) | |
end | |
def set(a, d, s, r) | |
@a = a | |
@d = d | |
@s = s | |
@r = r | |
end | |
def call(seconds, freq) | |
wave = oscillator.call(seconds, freq) | |
apply(wave) | |
wave | |
end | |
def notify(msg, *args) | |
case msg | |
when :envelope | |
set(*args) | |
true | |
when :init | |
set(*@orig) | |
true | |
else | |
oscillator.notify(msg, *args) | |
end | |
end | |
def apply(wave) | |
return if @a == 0 && @d == 0 && @s == 1 && @r == 0 | |
len = wave.size.to_f | |
ta = len * @a | |
td = len * @d | |
ls = @s | |
tr = len * @r | |
filters = [ | |
[1 / ta, 0, ta], | |
[(ls - 1) / td, 1 + ta * (1 - ls) / td, ta + td], | |
[0, ls, len - tr], | |
[-ls / tr, len * ls / tr, len], | |
] | |
wave.filter_with_index! do |w,i| | |
filters.each do |a, b, limit| | |
if i < limit | |
w *= a * i + b | |
break | |
end | |
end | |
w | |
end | |
end | |
end | |
class Synthesizer | |
attr_accessor :oscillator, :octave, :bpm, :meter | |
Notes = %w/a b h c cis d dis e f fis g gis/.map(&:intern) | |
def initialize(oscillator=Oscillator.new) | |
@oscillator = oscillator | |
init | |
end | |
def play(expr) | |
init | |
waves = interpret(expr) | |
waves.compact! | |
Wave.concat!(*waves) | |
end | |
private | |
def init | |
@bpm = 60 | |
@octave = 4 | |
@meter = 1 | |
@oscillator.notify(:init) | |
end | |
def play_freq(seconds, freq) | |
@oscillator.call(seconds, freq) | |
end | |
# 参考: | |
# http://en.wikipedia.org/wiki/Scientific_pitch_notation | |
# http://en.wikipedia.org/wiki/A_(musical_note) | |
def play_pitch(seconds, pitch) | |
play_freq(seconds, 440 * 2 ** ((pitch - 69) / 12.0)) | |
end | |
# play_note(1, 4, :a) = play_pitch(1, 69) = play_freq(1, 440) | |
def play_note(seconds, octave, note_name) | |
note = Notes.index(note_name) and | |
play_pitch(seconds,12 * octave + note + 21) | |
end | |
def play_mute(seconds) | |
play_freq(seconds, 0) | |
end | |
def interpret(expr) | |
expr.map {|e| play_expr(e) } | |
end | |
def play_expr(e) | |
case e.fetch(0) | |
when :bpm | |
@bpm = e.fetch(1) | |
nil | |
when :meter | |
@meter = e.fetch(1) | |
nil | |
when :octave | |
@octave = e.fetch(1) | |
nil | |
when :par | |
waves = interpret(e[1..-1]) | |
waves.compact! | |
Wave.add!(*waves).normalize! | |
when :seq | |
waves = interpret(e[1..-1]) | |
waves.compact! | |
Wave.concat!(*waves) | |
else | |
return nil if @oscillator.notify(*e) | |
note_name = e[0] | |
beats = e.fetch(1) | |
seconds = beats * 60.0 / @bpm * @meter | |
if :rest === note_name | |
play_mute(seconds) | |
else | |
play_note(seconds, octave, note_name) | |
end | |
end | |
end | |
end | |
def parse(str) | |
s = StringScanner.new(str) | |
stack = [[]] | |
until s.eos? | |
s.skip(/\s*/) | |
if s.skip(/[(\[]/) | |
stack.push [] | |
elsif s.skip(/[)\]]/) | |
raise "括弧の始まりが見当たらない" if stack.size == 1 | |
top = stack.pop | |
stack.last << top | |
elsif s.skip(/(-?\d+)\/(\d+)/) | |
stack.last << Rational(s[1].to_i, s[2].to_i) | |
elsif s.skip(/-?\d+\.\d+/) | |
stack.last << s.matched.to_f | |
elsif s.skip(/-?\d+/) | |
stack.last << s.matched.to_i | |
elsif s.skip(/"[^"\\]*(?:\\.[^"\\]*)*"/) | |
stack.last << eval(s.matched.gsub("\\", "\\\\")) | |
elsif s.skip(/#r"[^"\\]*(?:\\.[^"\\]*)*"/) | |
stack.last.concat(*eval(eval(s.matched[3..-1].gsub("\\", "\\\\")))) | |
elsif s.skip(/[^()\[\]\"\s]+/) | |
stack.last << s.matched.intern | |
elsif !s.eos? | |
raise "unknown pattern: #{s.peek(10).inspect}.." | |
end | |
end | |
raise "閉じ括弧が足りない" if stack.size != 1 | |
stack[0] | |
end | |
if $0 == __FILE__ | |
synth = Synthesizer.new(Envelope.new(Oscillator.new)) | |
e = parse(DATA.read) | |
e.each do |name, *rest| | |
riff = synth.play(rest).normalize!.to_riff | |
open(name, "wb") {|f| f << riff } | |
end | |
end | |
__END__ | |
("yes4.wav" | |
(envelope 0 0.1 0.5 0.6) | |
(waveform 0 0.5 1 -1 -0.5) | |
(bpm 60) | |
(octave 5) | |
(e 1/8) (c 1/8) (e 1/8) (c 1/2)) | |
("no4.wav" | |
(envelope 0 0 0.1 0.05) | |
(waveform 1 -1) | |
(bpm 60) | |
(octave 1) | |
(c 1/8) (c 1/2)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment