-
-
Save paulsonkoly/57b871b21c4d20092b27d575e16e8a3f to your computer and use it in GitHub Desktop.
sandstorm
This file contains hidden or 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
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