Skip to content

Instantly share code, notes, and snippets.

@paulsonkoly
Last active June 16, 2020 15:41
Show Gist options
  • Save paulsonkoly/57b871b21c4d20092b27d575e16e8a3f to your computer and use it in GitHub Desktop.
Save paulsonkoly/57b871b21c4d20092b27d575e16e8a3f to your computer and use it in GitHub Desktop.
sandstorm
require 'tempfile'
module Music
refine Float do
def hz
self
end
def khz
self * 1000
end
def second
self
end
def seconds
self
end
def beat
self
end
end
using Music
SAMPLE_RATE = 48.0.khz
PITCH_STANDARD = 440.0.hz
TWELFTH_ROOT_OF_TWO = 2 ** ( 1.0 / 12 )
BEATS_PER_MINUTE = 120
def self.semitone(n = 0)
PITCH_STANDARD * TWELFTH_ROOT_OF_TWO ** n
end
A4 = 0
A4_SHARP = 1
B4 = 2
C5 = 3
C5_SHARP = 4
D5 = 5
D5_SHARP = 6
E5 = 7
F5 = 8
F5_SHARP = 9
G5 = 10
G5_SHARP = 11
class Note
def initialize(pitch: 440.0.hz, volume: 0.5, duration: 1.0.beat,
attack: 0.2, decay: 0.2, release: 0.2)
@pitch = pitch
@volume = volume
@duration = duration
@step = (@pitch * 2 * Math::PI) / SAMPLE_RATE
@attack = attack
@decay = decay
@release = release
@adsr_sustain = 0.2
end
def time
60.0.seconds / BEATS_PER_MINUTE * @duration
end
def data
(0..samples).map do |e|
beat_control(e) * @volume * Math.sin(e * @step)
end
end
def to_bytestring
data.pack('e*')
end
private
# /`--.
# ads r
def beat_control(ix)
where = ix / samples
case
when where < @attack then where / @attack
when where < (@attack + @decay) then 1.0 - (where - @attack) * (1.0-@adsr_sustain) / @decay
when where < 1 - @release then @adsr_sustain # sustain
else (1.0 - @release - where) * @adsr_sustain / @release + @adsr_sustain
end
end
def samples
SAMPLE_RATE * time
end
end
NOTES = 12.times.map do |ix|
Note.new(pitch: semitone(ix), duration: 1.0.beat).freeze
end
NOTES_HALF = 12.times.map do |ix|
Note.new(pitch: semitone(ix), duration: 0.5.beat).freeze
end
NOTES_QUARTER = 12.times.map do |ix|
Note.new(pitch: semitone(ix), duration: 0.25.beat).freeze
end
class Wave
def initialize(*notes)
@notes = notes
end
def play
with_tempfile do |io|
io.write(to_bytestring)
io.close
system("ffplay -showmode 1 -f f32le -ar #{SAMPLE_RATE} #{io.path}")
end
end
private
def with_tempfile
io = Tempfile.new(__FILE__)
yield(io)
ensure
io.close
io.unlink
end
def to_bytestring
@notes.map(&:to_bytestring).join
end
end
MUSIC = Wave.new(
*4.times.map { NOTES_QUARTER[A4] },
NOTES_HALF[A4],
*6.times.map { NOTES_QUARTER[A4] },
NOTES_HALF[A4],
*6.times.map { NOTES_QUARTER[D5] },
NOTES_HALF[D5],
*6.times.map { NOTES_QUARTER[C5] },
NOTES_HALF[C5],
Note.new(pitch: semitone(-2), duration: 0.5.beat),
*4.times.map { NOTES_QUARTER[A4] },
NOTES_HALF[A4]
)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment