Skip to content

Instantly share code, notes, and snippets.

@tompng
Last active April 24, 2025 15:34
Show Gist options
  • Save tompng/0d87c9af788d7672c896668e87dc9cb8 to your computer and use it in GitHub Desktop.
Save tompng/0d87c9af788d7672c896668e87dc9cb8 to your computer and use it in GitHub Desktop.
sound generation
require 'chunky_png'
require 'numo/narray'
file = ARGV[0]
data = File.binread(file)
wave = data[44..].unpack('v*').map do |v|
(v <= 0x7fff ? v : v - 0x10000).fdiv(0x8000)
end
RATE = 44100
sec = ARGV[1].to_i.nonzero? || 5
wave = wave.take(RATE * sec)
wave = Numo::NArray.cast(wave)
pix_per_sec = 512
num_hz = 512
hz_min = 100
hz_max = 10000
width = wave.size / RATE * pix_per_sec
height = num_hz
img = ChunkyPNG::Image.new(width, height)
height.times do |y|
hz = hz_max * hz_min.fdiv(hz_max) ** y.fdiv(num_hz - 1)
wlen = RATE.fdiv(hz)
cwave = wave * (1i ** (4.0 / wlen)) ** Numo::Complex32.new(wave.size).seq
cwave_sum = cwave.cumsum
len = (wlen * num_hz / Math.log2(hz_max.fdiv(hz_min)) / 8).round
cwave_av = [
cwave_sum[len...2*len],
cwave_sum[2 * len...cwave_sum.size] - cwave_sum[0...cwave_sum.size - 2*len],
cwave_sum[cwave_sum.size - 1] - cwave_sum[cwave_sum.size - 2 * len...cwave_sum.size - len]
].reduce(:concatenate).abs / (2 * len + 1)
cumsum = cwave_av.cumsum
width.times do |x|
from = x * wave.size / width
to = (x + 1) * wave.size / width
v = (cumsum[to - 1] - cumsum[from]) / (to - from)
img[x, y] = ChunkyPNG::Color.rgb((1000 * v).round.clamp(0, 255), (10000 * v).round.clamp(0, 255), (100000 * v).round.clamp(0, 255))
end
p y
end
img.save('out.png')
require_relative './replace_vars.rb'
code = replace_vars(File.read('sound.rb')).gsub(/^ +|\n/, '')
puts code
puts code.size
height=40
width=80
canvas = height.times.map{[0]*width}
set=->x,y,v{
x=x.round;y=y.round
canvas[y][x]=v if 0<=x&&x<width&&0<=y&&y<height
}
fillcircle=->(cx,cy,cr,v,&b){
height.times{|y|width.times{|x|
r=(((x-cx).fdiv(width)*2)**2+((y-cy).fdiv(height)*2)**2)**0.5
set[x,y,v] if r<cr && (b ? b.(x-cx,y-cy,r,cr) : true)
}}
}
testproc = ->x,y,r,cr{
(Math.atan2(y,x)+0.7).abs > 0.6 || (r/cr-0.7).abs > 0.08
}
fillcircle[60.5,25.5,0.485,1,&testproc]
fillcircle[21.5,17.5,0.676,0]
fillcircle[21.5,17.5,0.547,1,&testproc]
fillcircle[58,7,0.37,1,&testproc]
hoge = canvas.map{|l|l.map{' #'[_1]}.join}.join($/)
puts hoge
p hoge.count'?'
original_code = code
chars = original_code.chars
$c = hoge.sub(/\A +\#{6}/, '').sub(/\#{9} *\n *\#{8}\s*\z/, '')
$c.gsub!('#'){chars.shift||';'}
code2 = ' '*54+'$c=%q@'+$c+"@;eval$c.#{' '*10}\n#{' '*57}split*''"+' '*15
puts code2
File.write 'seashore.rb', code2+"\n"
def eval_in_method(code)=eval(code)
eval_in_method(code2)

Seashore - Nature Sound

Listen to the relaxing sound of ocean waves generated by Ruby.

Usage

ruby entry.rb
ruby entry.rb seashore.wav 60

The default filename is output.wav and the default duration is 30 seconds.

Tested with ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +MN +PRISM [arm64-darwin22]

Noise

Noise sound is created by applying low-pass/band-pass filter to a white noise signal. Volume of the noise should change over time. This is also calculated by using low-pass filter.

Wave

Sound of a single ocean wave is composed of hundreds of water splash sounds. Each water splash sound is composed of thousands of water drops and bubbles. This program creates this complicated sound by repeating sound mixing.

sound2 = mix(sound1.change_pitch(rand), sound1.change_pitch(rand).delay(rand))
sound4 = mix(sound2.change_pitch(rand), sound2.change_pitch(rand).delay(rand))
...
sound32768 = mix(sound16384.change_pitch(rand), sound16384.change_pitch(rand).delay(rand))

splash1 = mix(sound1.delay(rand), sound2.delay(rand), sound4.delay(rand), ..., sound16384.delay(rand))
splash2 = mix(splash1.change_pitch(rand), splash1.change_pitch(rand).delay(rand))
splash4 = mix(splash2.change_pitch(rand), splash2.change_pitch(rand).delay(rand))
...
splash1024 = mix(splash512.change_pitch(rand), splash512.change_pitch(rand).delay(rand))

wave_sound = [splash32, splash64, ..., splash1024].sample

This kind of repetition is often used in fractal rendering. In other words, this operation is rendering a fractal shape to the spectrogram canvas. It's an efficient way to create complex structure with low computational cost.

def replace_vars(code)
code.gsub(/[0-9a-zA-Z_]+/){|a|
{
'wv' => 'j',
'cw' => 'k',
'lr' => 'm',
'n1' => 'u',
'n2' => 'v',
'n3' => 'w',
'n4' => 'x',
'sm' => 'y',
'sn' => 'z',
}[a] || a
}
end
def replace_vars_test(type)
ARGV.replace(['output.wav', '30'])
srand 0
case type
in :source
$c=40.times.map{'x'*80+"\n"}.join
eval replace_vars(File.read('./sound.rb'))
in :output
eval File.read('./seashore.rb')
end
output = `md5 output.wav`
expected = '29b462e87b17bbd6e27c9fac63dfb4d5'
if output.include? expected
puts 'OK'
puts output
else
puts 'Wrong md5'
puts output
p(expected:)
end
end
if $0 == __FILE__
replace_vars_test(ARGV[0].to_sym)
end
$c=%q@E="
\e[4%d;37m%s\e[m"
;n=32.chr;pu ts"\e
[H\e[J#{$c=n*54+' $c=%
q'+[64.chr]*2*$c+';e val$
c.'+n*10+"\n"+n*57+"spl it*'
'"+n*15}";n=l=0;R=->y=0 {n+=1
;l=$c.lines. map{|m|m=(0..79).chunk{380-n+
36*Math.sin(0.04.*it-n )<9*y}.map{a=_2.map{m[it]}*''
;_1&&E%[6,a]||a}*'';m!=l[~-y +=1]&&$><<"\e[#{y}H#{m}\e[37H
";m}};N=(Integer$* [-1]resc ue+30)*H=44100;alias:r:rand
;F=->e,w=1{a=b=c=0;d=( 1-e)**0 .5*20;->v=r-0.5{a=a*w*e+v
;b=b*w*e*e+v;d.*a-2*b+c=c*w *e**3+ v}};A=->u,n,t{(0..n).
map{|i|u=u.shuffle.map{|w|R[]; a=u.s ample;b,c,d=[[0.5
,(0.2+r)*H/3*1.1**i,[[1+r/10,1+r/ 10]][ i]||[1.2+
r/10,1.3+r/5]],[0.3,r*H/2,[1,1+r/5 ]]][t
];e,f=d.shuffle;g=b+r;h=b+r;(0..[w. size/e, a.size/f
+c].max).map{g*(w[it*e]||0)+h*(a[[it-c,0].ma x*f]||0)}}}};j=A[A
[(0..9).map{a=F[0.998,1i**0.02];(0..28097).m ap{a[].real.*0.1**(8.0*i
t/H)-8e-6}},14,0].transpose.map{|d|a=[0]*3e3 ;15.times{|i|R [];b=r
(3e3);d[i].each_with_index{a[c=_2+b]=(a[c] ||0)+_1*0.63**i}} ;a},9,
1][4..].flatten(1).shuffle;y=(0..3).map{F[ 1-1e-5]};m=[-1,1].map {[F[1
-1e-4],F[1-5e-5],it]};u=v=w=0;k=[],[],[] ;z=F[0.7,1i**0.5];File.o pen($
*.grep(/[^\d]/)[0]||'output.wav','wb') {|f|f<<'RIFF'+[N*4+36,'WA VEfmt
',32,16,1,2,H,H*4,4,16,'data',N*4].p ack('Va7cVvvVVvva4V');N.tim es{|
i|$><<E%[4,?#]if(i+1)*80/N!=i*80 /N;t=[i/1e5,(N-i)/2e5,1].min;a,b,c=k
.map{it.shift||(j[20*r,0]=[g =j.pop];a=1+r/3;it[0..]=(0..g.size).m
ap{g[it*a]||0};0)};u=u *0.96+r-0.5;v=v*0.99+d=r-0.5;w=w*0.8+d
;x=(z[].*1+0 .59i).imag;e=y.map(&:[]);f.<<m.map{|o,
p,q|r=a+(b+c)/2+(b-c)*q/5;s=o[r.abs]
;r=t*t*(3-2*t)*(r+s*w/1e4+p[s]*x/1
e7+[[u,0],[v,1]].sum{_1*1.5**(e[
_2]+q*e[_2+2]/9)}/32)/9;r/(1
+r*r)**0.5*32768}.pack'v
*'}};puts@;eval$c.
split*''
E="\e[4%d;37m%s\e[m";
n=32.chr;
puts"\e[H\e[J#{$c=n*54+'$c=%q'+[64.chr]*2*$c+';eval$c.'+n*10+"\n"+n*57+"split*''"+n*15}";
n=l=0;
R=->y=0{
n+=1;
l=$c.lines.map{|m|
m=(0..79).chunk{380-n+36*Math.sin(0.04.*it-n)<9*y}.map{a=_2.map{m[it]}*'';_1&&E%[6,a]||a}*'';
m!=l[~-y+=1]&&$><<"\e[#{y}H#{m}\e[37H";
m
}
};
N=(Integer$*[-1]rescue+30)*H=44100;alias:r:rand;
F=->e,w=1{
a=b=c=0;
d=(1-e)**0.5*20;
->v=r-0.5{
a=a*w*e+v;
b=b*w*e*e+v;
d.*a-2*b+c=c*w*e**3+v
}
};
A=->u,n,t{
(0..n).map{|i|
u=u.shuffle.map{|w|
R[];
a=u.sample;
b,c,d=[
[0.5,(0.2+r)*H/3*1.1**i,[[1+r/10,1+r/10]][i]||[1.2+r/10,1.3+r/5]],
[0.3,r*H/2,[1,1+r/5]]
][t];
e,f=d.shuffle;
g=b+r;
h=b+r;
(0..[w.size/e,a.size/f+c].max).map{
g*(w[it*e]||0)+h*(a[[it-c,0].max*f]||0)
}
}
}
};
wv=A[
A[
(0..9).map{
a=F[0.998,1i**0.02];(0..28097).map{a[].real.*0.1**(8.0*it/H)-8e-6}
},14,0
].transpose.map{|d|
a=[0]*3e3;15.times{|i|R[];b=r(3e3);d[i].each_with_index{a[c=_2+b]=(a[c]||0)+_1*0.63**i}};a
},9,1
][4..].flatten(1).shuffle;
sm=(0..3).map{F[1-1e-5]};
lr=[-1,1].map{[F[1-1e-4],F[1-5e-5],it]};
n1=n2=n3=0;
cw=[],[],[];
sn=F[0.7,1i**0.5];
File.open($*.grep(/[^\d]/)[0]||'output.wav','wb'){|f|
f<<'RIFF'+[N*4+36,'WAVEfmt',32,16,1,2,H,H*4,4,16,'data',N*4].pack('Va7cVvvVVvva4V');
N.times{|i|
$><<E%[4,?#]if(i+1)*80/N!=i*80/N;
t=[i/1e5,(N-i)/2e5,1].min;
a,b,c=cw.map{
it.shift||(
wv[20*r,0]=[g=wv.pop];
a=1+r/3;
it[0..]=(0..g.size).map{g[it*a]||0};0
)
};
n1=n1*0.96+r-0.5;
n2=n2*0.99+d=r-0.5;
n3=n3*0.8+d;
n4=(sn[].*1+0.59i).imag;
e=sm.map(&:[]);
f.<<lr.map{|o,p,q|
r=a+(b+c)/2+(b-c)*q/5;
s=o[r.abs];
r=t*t*(3-2*t)*(r+s*n3/1e4+p[s]*n4/1e7+[[n1,0],[n2,1]].sum{_1*1.5**(e[_2]+q*e[_2+2]/9)}/32)/9;
r/(1+r*r)**0.5*32768
}.pack'v*'
}
};puts
module WAV
def self.towav(wave)
[
'RIFF',
[wave.size * 2 + 36].pack('V'),
'WAVE', # 4
'fmt ', # 4
[16, 1, 1, 44100, 88200, 2, 16].pack('VvvVVvv'), # 20
'data', # 4
[wave.size * 2].pack('V')
].join + wave.map do |v|
(v/(1+v*v)**0.5*32768).floor
end.pack('v*')
end
def self.normalize(wave)
max = wave.minmax.map(&:abs).max
wave.map { |v| v / max }
end
def self.save(file, wave)
File.write(file, towav(normalize(wave)))
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment