Skip to content

Instantly share code, notes, and snippets.

@snipsnipsnip
Forked from koguro/scheme_baton.scm
Last active August 24, 2019 16:10
Show Gist options
  • Save snipsnipsnip/1364375 to your computer and use it in GitHub Desktop.
Save snipsnipsnip/1364375 to your computer and use it in GitHub Desktop.
ruby に移植 (第1回 Scheme コードバトンの成果のsynthesizer)
# 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
# ベタ移植から自分の好みにあわせて整理した版
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