Skip to content

Instantly share code, notes, and snippets.

@apeiros
Created November 6, 2011 19:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save apeiros/1343327 to your computer and use it in GitHub Desktop.
Save apeiros/1343327 to your computer and use it in GitHub Desktop.
Fun with pure tones and WAV (see code after __END__)
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