Created
November 6, 2011 19:10
-
-
Save apeiros/1343327 to your computer and use it in GitHub Desktop.
Fun with pure tones and WAV (see code after __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
class Numeric; def cap(min,max) return min if self < min; return max if self > max; self; end; end | |
class Wav | |
module Generators | |
end | |
def self.mnot(str) | |
tones = { | |
'C' => 264.0, | |
'D' => 297.0, | |
'E' => 330.0, | |
'F' => 352.0, | |
'G' => 396.0, | |
'A' => 440.0, | |
'H' => 495.0, | |
} | |
durations = Hash[('a'..'z').zip(26.times.map { |i| 10*(2**i) })] | |
samples = str.scan(/([A-Z_])(\d)?\.(?:(\d+)|([A-Z]))/).flat_map { |note, note_level, duration_ms, duration_letter| | |
duration_in_ms = duration_ms ? duration_ms.to_i : durations[duration_letter.downcase] | |
if note == '_' then | |
Array.new((duration_in_ms*44.1).round, 0) | |
else | |
multiplier = 2**((note_level || 4).to_i - 4) | |
frequence = tones[note.upcase]*multiplier | |
Sequence.sinus_samples(16, 44100, frequence, 1, duration_in_ms) | |
end | |
} | |
mono_cd_from_samples(samples) | |
end | |
def self.mono_cd_from_samples(samples) | |
audio_size = samples.size*2 | |
meta = { | |
:wave_chunk_id => "RIFF", | |
:wave_chunk_size => audio_size+36, | |
:wave_id => "WAVE", | |
:format_chunk_id => "fmt ", | |
:format_chunk_size => 16, | |
:format_tag => 1, | |
:channels => 1, | |
:samples_per_second => 44100, | |
:average_bytes_per_second => 88200, | |
:block_align => 4, | |
:bits_per_sample => 16, | |
:data_chunk_id => "data", | |
:data_chunk_size => audio_size | |
} | |
new(meta, samples) | |
end | |
def self.intize(sequence, min, max) | |
sequence.map { |value| value.round.cap(min, max) } | |
end | |
def self.stereo(mono) | |
mono.flat_map { |x| [x,x] } | |
end | |
def self.low_pass(sequence, dt, rc) | |
result = sequence[0,1] | |
alpha = dt.fdiv(rc+dt) | |
1.upto(sequence.length-1) { |i| | |
result[i] = result[i-1] + alpha * (sequence[i] - result[i-1]) | |
} | |
result | |
end | |
class Sequence | |
MaxAmplitude = proc { |seq, ms| 1 } unless defined? MaxAmplitude | |
MinAmplitude = proc { |seq, ms| 0 } unless defined? MinAmplitude | |
FrequencyA440 = proc { |seq, ms| Math.sin(ms.fdiv(1000)*440*2*Math::PI) } unless defined? FrequencyA440 | |
attr_reader :bits_per_sample | |
attr_reader :samples_per_second | |
attr_reader :frequence | |
attr_reader :amplitude | |
attr_reader :max_amplitude | |
attr_reader :min_amplitude | |
def self.sinus_samples(bits_per_sample, samples_per_second, frequence, amplitude, duration_in_ms) | |
samples_per_ms = samples_per_second.fdiv(1000) | |
samples_count = (duration_in_ms*samples_per_second*0.001).round | |
max_amplitude = (1<<(bits_per_sample-1))-1 | |
min_amplitude = 1<<(bits_per_sample-1) | |
min_neg_amplitude = -min_amplitude | |
factor = 0.002*Math::PI*frequence | |
(0...samples_count).map { |i| | |
ms = i.fdiv(samples_per_ms) | |
pulse = Math.sin(ms*factor) | |
value = pulse*amplitude*(pulse < 0 ? min_amplitude : max_amplitude) | |
value.round.cap(min_neg_amplitude, max_amplitude) | |
} | |
end | |
# The frequency proc should generate a value between -1 and 1 | |
# The amplitude proc should generate a value between 0 and 1 | |
def initialize(bits_per_sample, samples_per_second, frequence, amplitude) | |
@bits_per_sample = bits_per_sample | |
@samples_per_second = samples_per_second | |
@frequence = frequence | |
@amplitude = amplitude | |
@max_amplitude = (1<<(bits_per_sample-1))-1 | |
@min_amplitude = 1<<(bits_per_sample-1) | |
@min_neg_amplitude = -@min_amplitude | |
end | |
def sample(duration_in_ms) | |
samples = (duration_in_ms*samples_per_second).fdiv(1000).round | |
samples_per_ms = samples_per_second.fdiv(1000) | |
(0...samples).map { |i| | |
ms = i.fdiv(samples_per_ms) | |
pulse = @frequence.call(self, ms) | |
amplitude = @amplitude.call(self, ms) | |
value = pulse*amplitude*(pulse < 0 ? @min_amplitude : @max_amplitude) | |
value | |
} | |
end | |
def sample_stereo(duration_in_ms) | |
Wav.stereo(sample(duration_in_ms)) | |
end | |
end | |
Fields = [ | |
:wave_chunk_id, | |
:wave_chunk_size, | |
:wave_id, | |
:format_chunk_id, | |
:format_chunk_size, | |
:format_tag, | |
:channels, | |
:samples_per_second, | |
:average_bytes_per_second, | |
:block_align, | |
:bits_per_sample, | |
:cb_size, | |
:valid_bits_per_sample, | |
:dw_channel_mask, | |
:sub_format, | |
:data_chunk_id, | |
:data_chunk_size, | |
] unless defined? Fields | |
def self.read_file(path) | |
self.parse(File.read(path, :encoding => Encoding::BINARY)) | |
end | |
def self.parse(string) | |
meta_data = Hash[Fields[0,11].zip(string[0,36].unpack("A4IA4A4ISSIISS"))] | |
raise "not implemented" unless meta_data[:format_chunk_size] == 16 | |
raise "not implemented" unless meta_data[:format_tag] == 1 | |
#raise "not implemented" unless meta_data[:channels] == 2 | |
raise "not implemented" unless meta_data[:bits_per_sample] == 16 | |
meta_data.update(Hash[Fields[-2,2].zip(string[36,8].unpack("A4I"))]) | |
new(meta_data, string[44..-1].unpack("s*")) | |
end | |
attr_reader :meta_data, :data | |
def initialize(meta_data, data) | |
@meta_data = meta_data | |
@data = data | |
end | |
def to_binary | |
@meta_data.values_at(*Fields).compact.pack("A4IA4A4ISSIISSA4I")+@data.pack("s*") | |
end | |
def to_file(path) | |
File.open(path, "wb:binary") do |fh| | |
fh.write(to_binary) | |
end | |
end | |
end | |
__END__ | |
# Generate a file alle_meine_entlein.wav in your home directory. Contains the german children's song "Alle meine Entlein" | |
load('./wav.rb') | |
w = Wav.mnot(<<EONOT) | |
C.F D.F E.F F.F _.A G.G _.A G.G _.A | |
A.F _.A A.F _.A A.F _.A A.F _.A G.H _.A | |
A.F _.A A.F _.A A.F _.A A.F _.A G.H _.A | |
F.F _.A F.F _.A F.F _.A F.F _.A E.G _.A E.G _.A | |
D.F _.A D.F _.A D.F _.A G.F _.A C.H | |
EONOT | |
w.to_file(File.expand_path('~/alle_meine_entlein.wav')) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment