-
Notifications
You must be signed in to change notification settings - Fork 15
/
nanosynth.rb
108 lines (95 loc) · 3.82 KB
/
nanosynth.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
### Nanosynth
### Copyright (C) 2014, 2016-19 Joel Strait
###
###
### This is a simple sound generator capable of creating sound based on
### five types of wave: sine, square, sawtooth, triangle, and noise.
###
### This is intended for educational purposes, to show an example
### of how to create sound using Ruby. Clarity has been favored over
### performance, error-handling, succinctness, etc.
###
###
### Before using, first install the WaveFile gem:
###
### gem install wavefile --version 1.1.1
###
### And then to use, run `ruby nanosynth.rb` with these arguments:
###
### ruby nanosynth.rb <waveform> <frequency> <amplitude>
###
### For example:
###
### ruby nanosynth.rb sine 440.0 0.2
###
### This will create a Wave file called "mysound.wav" in the current
### working directory, containing a 440Hz sine wave at 20% full volume.
### You should be able to play this file in pretty much any media player.
###
### If you're on a Mac, you can generate the sound and play it at the same time
### by using the afplay command:
###
### ruby nanosynth.rb sine 440.0 0.5 && afplay mysound.wav
###
###
### For more detail about how all of this works, check out this blog post:
###
### https://www.joelstrait.com/nanosynth/
gem "wavefile", "=1.1.1"
require "wavefile"
OUTPUT_FILENAME = "mysound.wav"
SAMPLE_RATE = 44100
SECONDS_TO_GENERATE = 1
TWO_PI = 2 * Math::PI
RANDOM_GENERATOR = Random.new
def main
# Read the command-line arguments.
waveform = ARGV[0].to_sym # Should be "sine", "square", "saw", "triangle", or "noise"
frequency = ARGV[1].to_f # 440.0 is the same as middle-A on a piano.
amplitude = ARGV[2].to_f # Should be between 0.0 (silence) and 1.0 (full volume).
# Amplitudes above 1.0 will result in clipping distortion.
# Generate sample data at the given frequency and amplitude.
# The sample rate indicates how many samples we need to generate for
# 1 second of sound.
num_samples = SAMPLE_RATE * SECONDS_TO_GENERATE
samples = generate_sample_data(waveform, num_samples, frequency, amplitude)
# Wrap the array of samples in a Buffer, so that it can be written to a Wave file
# by the WaveFile gem. Since we generated samples with values between -1.0 and 1.0,
# the sample format should be :float
buffer = WaveFile::Buffer.new(samples, WaveFile::Format.new(:mono, :float, SAMPLE_RATE))
# Write the Buffer containing our samples to a monophonic Wave file
WaveFile::Writer.new(OUTPUT_FILENAME, WaveFile::Format.new(:mono, :pcm_16, SAMPLE_RATE)) do |writer|
writer.write(buffer)
end
end
# The dark heart of NanoSynth, the part that actually generates the audio data
def generate_sample_data(waveform, num_samples, frequency, amplitude)
position_in_period = 0.0
position_in_period_delta = frequency / SAMPLE_RATE
# Initialize an array of samples set to 0.0. Each sample will be replaced with
# an actual value below.
samples = [].fill(0.0, 0, num_samples)
num_samples.times do |i|
# Add next sample to sample list. The sample value is determined by
# plugging position_in_period into the appropriate wave function.
if waveform == :sine
samples[i] = Math::sin(position_in_period * TWO_PI) * amplitude
elsif waveform == :square
samples[i] = (position_in_period >= 0.5) ? amplitude : -amplitude
elsif waveform == :saw
samples[i] = ((position_in_period * 2.0) - 1.0) * amplitude
elsif waveform == :triangle
samples[i] = amplitude - (((position_in_period * 2.0) - 1.0) * amplitude * 2.0).abs
elsif waveform == :noise
samples[i] = RANDOM_GENERATOR.rand(-amplitude..amplitude)
end
position_in_period += position_in_period_delta
# Constrain the period between 0.0 and 1.0.
# That is, keep looping and re-looping over the same period.
if position_in_period >= 1.0
position_in_period -= 1.0
end
end
samples
end
main