-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathquote2note.rb
218 lines (171 loc) · 6.86 KB
/
quote2note.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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#!/usr/bin/env ruby
### parse command line options
require 'trollop'
opts = Trollop::options do
version "quote2note 1.0 (c) 2014 Larry Lang"
banner <<-EOS
quote2note converts stock quotes to MIDI music.
Price maps to pitch, trend to harmony, and volume to loudness.
Usage:
quote2note [options] <symbol>
where [options] are:
EOS
opt :symbol, "Stock symbol", :type => String
opt :duration, "Music duration", :default => 36.5
opt :live, "Direct to synthesizer, otherwise to default MIDI file SYMBOL-DATE.mid"
opt :midifile, "Alternate MIDI file name", :type => String
end
# remove all non-letter characters and convert to uppercase
symbol = opts[:symbol].gsub(/[^a-zA-Z]/, "").upcase
duration = opts[:duration]
live = opts[:live]
### retrieve stock data from Yahoo
require 'open-uri'
require 'openssl'
require 'csv'
# validate stock symbol
# obsolete: validated stock symbol with Yahoo
# url = "http://finance.yahoo.com/d/quotes?s=" +symbol+ "&f=x"
# exchange = open(url).string
url = "https://www.google.com/finance?q=" +symbol
exchange = open(url, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE}) { |io| io.read }
if exchange.include? "did not match any finance results"
puts "ERROR: Invalid stock symbol [" +symbol+ "]"
exit
end
#obsolete section; shifted to input via bash script
#convert stock symbol into URL
#obsolete: download historical CSV for stock symbol from Yahoo
#url = "http://ichart.finance.yahoo.com/table.csv?s=" + symbol
#obsolete: download historical CSV for stock symbol from Google
#url = "https://www.google.com/finance/historical?output=csv&q=" + symbol
#qload = open(url, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE}) # { |io| io.read }
#quotes = CSV.read(qload)
# load stock data into into .csv array of arrays
# via bash script derived from https://github.com/bradlucas/get-yahoo-quotes
qload = %x(bash get-yahoo-quotes.sh #{symbol})
quotes = CSV.parse(qload)
quotes.shift # delete header row
# if needed, reverse so oldest to newest; otherwise comment out
# quotes = quotes.reverse
dates = quotes.map {|row| row[0]}
qprices = quotes.map {|row| row[1]} # opening price, to match TradingView chart
volumes = quotes.map {|row| row[6]}
count = dates.count
### map stock data to MIDI values
#calculate duration of each note
noteduration = duration/count
class Array
#define user Array method to scale from data to MIDI values, default 0 to 127
def midify(midimin=0, midimax=127)
$stderr.puts "Midimin: #{midimin}, Midimax: #{midimax}"
midispan = midimax - midimin
max = self.map(&:to_f).max
min = self.map(&:to_f).min
span = max - min
self.collect { |s| (((midispan/span)*(s.to_f-min))+midimin).round }
end
end
$stderr.puts "Midify quotes to notes"
notes = qprices.midify(24,84) # previous range from 0 to 120 too extreme
$stderr.puts "Midify volumes to velocities"
vels = volumes.midify(80,127)
#harmonies according to gaining or losing stock price trend
harmonies = Array.new(count)
harmonies.each_index do |i|
if i == 0 #if opening day
harmonies[i] = notes[i] #unison
elsif qprices[i] >= qprices[i-1] #if gaininig...
harmonies[i] = notes[i]+4 #major third
else #if losing...
harmonies[i] = notes[i]+6 #tritone
end
end
#pulses according to passing time periods
pulses = Array.new(count)
pulses.each_index do |i|
if i>0 && Date.parse(dates[i]).year != Date.parse(dates[i-1]).year
pulses[i] = 1
else
pulses[i] = 0
end
end
### write MIDI values via midilib to .mid file
# derivative of 'from_scratch.mid' example for midilib GEM
if live == false
require 'midilib/sequence'
require 'midilib/consts'
require 'midilib/io/seqwriter'
include MIDI
if ENV.has_key?('Q2N_DIR')
filepath = ENV['Q2N_DIR']+"/"
else
filepath = ""
end
filename = symbol+"-"+dates.last+".mid"
filefull = filepath + filename
seq = Sequence.new()
# Special first track holds tempo and other meta events
track = Track.new(seq)
seq.tracks << track
microduration = (noteduration*1000000).round #note duration in microseconds
track.events << Tempo.new(microduration) #tempo by (quarter) note duration
track.events << MetaEvent.new(META_SEQ_NAME, 'Quote2Note')
# Create a track to hold the notes. Add it to the sequence.
track = Track.new(seq)
seq.tracks << track
# Give the track a name and an instrument name (optional).
track.name = filename
track.instrument = GM_PATCH_NAMES[0]
# Add a volume controller event (optional).
track.events << Controller.new(0, CC_VOLUME, 127)
# Add note events to the track, according to MIDI values.
# Arguments for note on and note off constructors are
# channel, note, velocity, and delta_time.
# Channel numbers start at zero.
# Sequence#note_to_delta method yields delta time length of single quarter note.
track.events << ProgramChange.new(0, 1, 0)
quarter_note_length = seq.note_to_delta('quarter')
for i in 0..(count-1)
track.events << NoteOn.new(0, notes[i] , vels[i], 0)
track.events << NoteOn.new(0, harmonies[i] , vels[i], 0)
track.events << NoteOff.new(0, notes[i] , vels[i], quarter_note_length)
track.events << NoteOff.new(0, harmonies[i] , vels[i], 0)
#signal passing time periods
if pulses[i] == 1
track.events << NoteOn.new(9, 51, 64, 0)
end
end
File.open(filefull, 'wb') { |file| seq.write(file) }
puts filename
end
### play MIDI values via unimidi to synthesizer, external or software
if live == true
require 'unimidi'
# selects MIDI output
output = UniMIDI::Output.use(:first)
#set main patch on channel 1
output.puts(0xB0, 0x00, 121) #General MIDI patch bank
output.puts(0xB0, 0x20, 0)
output.puts(0xC0, 0x00 ) #General MIDI piano patch
#set click patch on channel 10
output.puts(0xB9, 0x00, 120) #General MIDI rhythm bank
output.puts(0xB9, 0x20, 0)
output.puts(0xC9, 0x00 ) #General MIDI standard rhythm
for i in 0..(count-1)
#print daily stock quote
puts "#{symbol.upcase} #{dates[i]} $#{qprices[i]} #{volumes[i]}"
#diagnostic
#puts "#{notes[i]} #{harmonies[i]} #{vels[i]} #{pulses[i]}"
#play note and harmony
output.puts(0x90, notes[i], vels[i]) # note on
output.puts(0x90, harmonies[i], vels[i]) # note on
sleep(noteduration) # wait
output.puts(0x80, notes[i], vels[i]) # note off
output.puts(0x80, harmonies[i], vels[i]) # note off
#signal passing time periods
if pulses[i] == 1
output.puts(0x99, 51, 64)
end
end
end