niki 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/.gitignore +3 -0
  2. data/Gemfile +7 -0
  3. data/Gemfile.lock +22 -0
  4. data/Rakefile +2 -0
  5. data/Readme.md +55 -0
  6. data/examples/my_song.rb +267 -0
  7. data/examples/my_song.rns +0 -0
  8. data/lib/core_ext/array.rb +13 -0
  9. data/lib/core_ext/fixnum.rb +8 -0
  10. data/lib/niki.rb +9 -0
  11. data/lib/niki/chords.rb +44 -0
  12. data/lib/niki/part.rb +69 -0
  13. data/lib/niki/song.rb +81 -0
  14. data/lib/niki/version.rb +3 -0
  15. data/niki.gemspec +23 -0
  16. data/vendor/archaeopteryx/archaeopteryx.rb +28 -0
  17. data/vendor/archaeopteryx/clip.rb +37 -0
  18. data/vendor/archaeopteryx/core_ext/array.rb +11 -0
  19. data/vendor/archaeopteryx/core_ext/struct.rb +7 -0
  20. data/vendor/archaeopteryx/drum.rb +24 -0
  21. data/vendor/archaeopteryx/live_hacks.rb +8 -0
  22. data/vendor/archaeopteryx/loop.rb +32 -0
  23. data/vendor/archaeopteryx/midi/clock.rb +23 -0
  24. data/vendor/archaeopteryx/midi/file_output/file_midi.rb +44 -0
  25. data/vendor/archaeopteryx/midi/note.rb +9 -0
  26. data/vendor/archaeopteryx/midi/practical_ruby_projects/core_foundation.rb +7 -0
  27. data/vendor/archaeopteryx/midi/practical_ruby_projects/core_midi.rb +16 -0
  28. data/vendor/archaeopteryx/midi/practical_ruby_projects/live_midi.rb +117 -0
  29. data/vendor/archaeopteryx/midi/practical_ruby_projects/no_midi_destinations.rb +8 -0
  30. data/vendor/archaeopteryx/midi/practical_ruby_projects/practical_ruby_projects.rb +30 -0
  31. data/vendor/archaeopteryx/midi/practical_ruby_projects/timer.rb +35 -0
  32. data/vendor/archaeopteryx/mix.rb +27 -0
  33. data/vendor/archaeopteryx/rhythm.rb +29 -0
  34. data/vendor/archaeopteryx/track.rb +16 -0
  35. metadata +113 -0
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in niki.gemspec
4
+ gemspec
5
+
6
+ gem 'unimidi'
7
+ # gem 'midiator', :path => '../midiator'
data/Gemfile.lock ADDED
@@ -0,0 +1,22 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ niki (0.0.1)
5
+ midilib
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ ffi (1.0.9)
11
+ ffi-coremidi (0.0.8)
12
+ ffi (>= 1.0)
13
+ midilib (2.0.0)
14
+ unimidi (0.2.1)
15
+ ffi-coremidi
16
+
17
+ PLATFORMS
18
+ ruby
19
+
20
+ DEPENDENCIES
21
+ niki!
22
+ unimidi
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/Readme.md ADDED
@@ -0,0 +1,55 @@
1
+ # Niki
2
+
3
+ Niki is a Ruby DSL to describe and play musical pieces.
4
+
5
+ It leverages Giles Bowkett's [Archaeopteryx](
6
+ https://github.com/gilesbowkett/archaeopteryx) to send MIDI output to other
7
+ programs accepting MIDI inputs (Ableton Live, Reason, Garage Band...), which
8
+ will actually play the song.
9
+
10
+ To see what the DSL looks like, take a look at the [example song](https://github.com/txus/niki/blob/master/examples/my_song.rb)
11
+
12
+ https://github.com/gilesbowkett/archaeopteryx/blob/master/db_drum_definition.rb
13
+
14
+ ## Caveats
15
+
16
+ * It runs only in Ruby 1.8.7 (due to the dependency on Archaeopteryx) :(
17
+ * I've tested this only in Snow Leopard, but it should run in more platforms.
18
+ If your platform is not supported it's because Archaeopteryx
19
+ doesn't support it yet.
20
+
21
+ ## Run the example song
22
+
23
+ First of all, get the example song files (both the Reason file and the Ruby file)
24
+
25
+ $ curl -o my_song.rns https://raw.github.com/txus/niki/master/examples/my_song.rns
26
+ $ curl -o my_song.rb https://raw.github.com/txus/niki/master/examples/my_song.rb
27
+
28
+ Now [download a demo version](
29
+ http://www.propellerheads.se/download/index.cfm?fuseaction=get_article&article=reason) of Propellerhead's
30
+ Reason 5 and open up `my_song.rns`.
31
+
32
+ ### Configuring the midi interface
33
+
34
+ This is how you configure the midi interface in Mac OSX (it should not be
35
+ difficult in other operating systems):
36
+
37
+ * Make sure the IAC driver is activated in Mac OSX's Audio/MIDI Setup (inside the
38
+ MIDI section) (activate the "Device is online" checkbox if it is not)
39
+ * In this section, make sure at least one port is created (under the Ports
40
+ section). If not, add one clicking the + button.
41
+ * Go to the Reason options panel, go to Advanced, and set your first
42
+ MIDI bus (Bus A) to "IAC Driver IAC Bus 1" (the port you just created).
43
+
44
+ ### Install the gem and run!
45
+
46
+ Now you should install the gem and run the example!
47
+
48
+ $ gem install niki
49
+ $ ruby my_song.rb
50
+
51
+ # TODO
52
+
53
+ * Tests + refactor
54
+ * Documentation
55
+ * Expand this README
@@ -0,0 +1,267 @@
1
+ require 'rubygems'
2
+ require 'niki'
3
+
4
+ song = Niki::Song.new :tempo => 127 do
5
+
6
+ # Configure midi channels
7
+ channel :basses, 1
8
+ channel :drums, 10
9
+ channel :chords, 2
10
+ channel :melodies, 3
11
+
12
+ configure :drums do |drums|
13
+ drums[:kick] = c1
14
+ drums[:snare] = c1.sharp
15
+ drums[:hh] = g1
16
+ drums[:ohh] = g1.sharp
17
+ end
18
+
19
+ part :intro do
20
+ # Drums
21
+ 2.times do
22
+ drum [:kick, :hh], 8
23
+ drum :hh, 8
24
+ drum [:snare, :hh], 8
25
+ drum :hh, 8
26
+
27
+ drum [:kick, :hh], 8
28
+ drum :hh, 8
29
+ drum [:snare, :hh], 8
30
+ drum [:kick, :hh], 8
31
+
32
+ drum :hh, 16
33
+ drum :hh, 16
34
+ drum :hh, 16
35
+ drum :hh, 16
36
+ drum [:snare, :hh], 8
37
+ drum :hh, 8
38
+
39
+ drum [:kick, :hh], 8
40
+ drum [:kick, :hh], 8
41
+ drum [:snare, :hh], 8
42
+ drum :ohh, 8
43
+ end
44
+
45
+ # Bass
46
+ bass f2, 4
47
+
48
+ bass f2, 8
49
+ bass e2, 8
50
+ bass f2, 8
51
+ bass e2, 8
52
+ bass f2, 8
53
+
54
+ bass e3, 4
55
+ bass c3, 4
56
+ bass b2, 8
57
+ bass c2, 8
58
+ bass c2, 8
59
+ bass g2, 4
60
+
61
+ bass a2, 4
62
+
63
+ bass a2, 8
64
+ bass g2, 8
65
+ bass a2, 8
66
+ bass g2, 8
67
+ bass a2, 8
68
+
69
+ bass e3, 4
70
+ bass c3, 4
71
+ bass b2, 8
72
+ bass c2, 8
73
+ bass c2, 8
74
+ bass g2, 4
75
+ end
76
+
77
+ part :intro_ending do
78
+ # Drums
79
+ drum [:kick, :hh], 8
80
+ drum :hh, 8
81
+ drum [:snare, :hh], 8
82
+ drum :hh, 8
83
+
84
+ drum [:kick, :hh], 8
85
+ drum :hh, 8
86
+ drum [:snare, :hh], 8
87
+ drum [:kick, :hh], 8
88
+
89
+ drum :hh, 16
90
+ drum :hh, 16
91
+ drum :hh, 16
92
+ drum :hh, 16
93
+ drum [:snare, :hh], 8
94
+ drum :hh, 8
95
+
96
+ drum [:kick, :hh], 8
97
+ drum [:kick, :hh], 8
98
+ drum [:snare, :hh], 8
99
+ drum :ohh, 8
100
+
101
+ # Drum break
102
+ drum [:kick, :hh], 8
103
+ drum :hh, 8
104
+ drum [:kick, :hh], 8
105
+ drum :hh, 8
106
+ drum [:kick, :hh], 8
107
+ drum :hh, 8
108
+ drum [:kick, :hh], 8
109
+ drum :hh, 8
110
+
111
+ drum [:snare, :ooh], 4
112
+ drum [:snare, :ooh], 4
113
+ drum :snare, 16
114
+ drum :snare, 16
115
+ drum :kick, 16
116
+ drum :kick, 16
117
+ drum :snare, 16
118
+ drum :snare, 16
119
+ drum [:snare, :ooh], 16
120
+ drum :snare, 16
121
+
122
+ # Bass
123
+ bass f2, 4
124
+
125
+ bass f2, 8
126
+ bass e2, 8
127
+ bass f2, 8
128
+ bass e2, 8
129
+ bass f2, 8
130
+
131
+ bass e3, 4
132
+ bass c3, 4
133
+ bass b2, 8
134
+ bass c2, 8
135
+ bass c2, 8
136
+ bass g2, 4
137
+
138
+ bass a2, 4
139
+ bass a2, 4
140
+ bass a2, 4
141
+ bass a2, 4
142
+
143
+ bass b2, 8
144
+ bass b2, 8
145
+ bass b2, 8
146
+ bass c2, 8
147
+ bass c2, 8
148
+ bass c2, 8
149
+ bass d3, 8
150
+ bass e3, 8
151
+ end
152
+
153
+ part :pre_chorus do
154
+ bass :from => :intro
155
+ drum :from => :intro
156
+
157
+ melody a2, 4
158
+ melody a2, 4
159
+ melody f3, 8
160
+ melody g3, 8
161
+ melody a3, 8
162
+ melody g3, 4
163
+ melody f3, 8
164
+ melody e3, 8
165
+ melody d3, 4
166
+ melody c3, 8
167
+ melody e3, 4
168
+
169
+ melody a2, 2
170
+ melody a2, 8
171
+ melody g2, 4
172
+ melody c2, 4
173
+
174
+ melody c3, 8
175
+ melody d3, 8
176
+ melody e3, 8
177
+ melody f3, 8
178
+ melody e3, 8
179
+ melody d3, 8
180
+ melody c3, 8
181
+ end
182
+
183
+ repeat :pre_chorus
184
+
185
+ part :chorus do
186
+ melody :from => :pre_chorus
187
+
188
+ # Bass
189
+ 2.times do
190
+ bass f2, 8
191
+ bass f3, 8
192
+ bass f2, 8
193
+ bass f3, 8
194
+ end
195
+ bass c2, 8
196
+ bass c3, 8
197
+ bass c2, 8
198
+ bass c3, 8
199
+
200
+ bass g2, 8
201
+ bass g3, 8
202
+ bass f2, 8
203
+ bass d2, 8
204
+
205
+ 2.times do
206
+ bass a1, 8
207
+ bass a2, 8
208
+ bass a1, 8
209
+ bass a2, 8
210
+ end
211
+ bass c1, 8
212
+ bass c2, 8
213
+ bass c1, 8
214
+ bass c2, 8
215
+
216
+ bass g1, 8
217
+ bass g2, 8
218
+ bass f1, 8
219
+ bass d2, 8
220
+
221
+ # Drums
222
+ 7.times do
223
+ drum [:kick, :hh], 8
224
+ drum :hh, 8
225
+ drum [:snare, :kick, :hh], 8
226
+ drum :hh, 8
227
+ end
228
+ drum :kick, 8
229
+ drum :hh, 16
230
+ drum :hh, 16
231
+ drum [:snare, :kick], 16
232
+ drum :snare, 16
233
+ drum [:kick, :ohh], 8
234
+
235
+ # Chords
236
+ chord a3MIN(2), 2, :base => f3
237
+ chord a3MIN(2), 8, :base => f3
238
+ chord a3MIN(2), 8, :base => f3
239
+ chord a3MIN(2), 8, :base => f3
240
+ chord c3MAJ, 4
241
+ chord c3MAJ, 4
242
+ chord c3MAJ, 8
243
+ chord g3MAJ, 2
244
+ chord [a3, b3, e4]
245
+ chord c3MAJ, 4
246
+ chord c3MAJ, 4
247
+ chord g3MAJ, 8
248
+ chord g3MAJ, 8
249
+ chord g3MAJ, 8, :base => f4
250
+ chord g3MAJ, 8, :base => e4
251
+ end
252
+
253
+ repeat :chorus
254
+
255
+ part :outro do
256
+ drum :from => :intro
257
+ bass :from => :intro
258
+ chord :from => :chorus
259
+ end
260
+
261
+ part :outro_ending do
262
+ bass :from => :intro
263
+ chord :from => :chorus
264
+ end
265
+ end
266
+
267
+ song.play
Binary file
@@ -0,0 +1,13 @@
1
+ class Array
2
+ def sharp
3
+ map do |element|
4
+ element + 1
5
+ end
6
+ end
7
+
8
+ def flat
9
+ map do |element|
10
+ element - 1
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ class Fixnum
2
+ def sharp
3
+ self + 1
4
+ end
5
+ def flat
6
+ self - 1
7
+ end
8
+ end
data/lib/niki.rb ADDED
@@ -0,0 +1,9 @@
1
+ require File.expand_path(File.dirname(__FILE__)) + "/../vendor/archaeopteryx/archaeopteryx"
2
+ require 'core_ext/fixnum'
3
+ require 'core_ext/array'
4
+ require 'niki/chords'
5
+ require 'niki/part'
6
+ require 'niki/song'
7
+
8
+ module Niki
9
+ end
@@ -0,0 +1,44 @@
1
+ module Niki
2
+ module Chords
3
+
4
+ (1..5).to_a.each do |octave|
5
+
6
+ %w(c d e f g a b).each_with_index do |c, idx|
7
+ rest = 0
8
+ if idx > 2
9
+ rest = 1
10
+ end
11
+ base = 24 + (12 * octave) + (idx * 2) - rest
12
+
13
+ define_method("#{c}#{octave}") do
14
+ base
15
+ end
16
+
17
+ define_method("#{c}#{octave}MAJ") do |*args|
18
+ inversion = args.first || 1
19
+ case inversion
20
+ when 1
21
+ [base, base + 4, base + 7]
22
+ when 2
23
+ [base + 4, base + 7, base + 12]
24
+ when 3
25
+ [base + 7, base + 12, base + 16]
26
+ end
27
+ end
28
+
29
+ define_method("#{c}#{octave}MIN") do |*args|
30
+ inversion = args.first || 1
31
+ case inversion
32
+ when 1
33
+ [base, base + 3, base + 7]
34
+ when 2
35
+ [base + 3, base + 7, base + 12]
36
+ when 3
37
+ [base + 7, base + 12, base + 15]
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
data/lib/niki/part.rb ADDED
@@ -0,0 +1,69 @@
1
+ module Niki
2
+ class Part
3
+ attr_reader :name, :chords, :drums, :basses, :melodies
4
+
5
+ include Niki::Chords
6
+
7
+ def initialize(name, song, &block)
8
+ @name = name
9
+ @song = song
10
+ @drum_notes = @song.drum_notes
11
+ @chords = []
12
+ @drums = []
13
+ @basses = []
14
+ @melodies = []
15
+ self.instance_eval(&block)
16
+ end
17
+
18
+ def chord note, duration = 1, options = {}
19
+ if note.is_a?(Hash) && note[:from]
20
+ copy_from_part(note[:from], :chords)
21
+ return
22
+ end
23
+
24
+ note.push options[:base]
25
+ @chords << [note, calculate_duration(duration)]
26
+ end
27
+
28
+ def drum note, duration = 1, options = {}
29
+ if note.is_a?(Hash) && note[:from]
30
+ copy_from_part(note[:from], :drums)
31
+ return
32
+ end
33
+
34
+ drum_note = [note].flatten.map do |n|
35
+ @drum_notes[n]
36
+ end
37
+ @drums << [drum_note, calculate_duration(duration)]
38
+ end
39
+
40
+ def bass note = nil, duration = 1, options = {}
41
+ if note.is_a?(Hash) && note[:from]
42
+ copy_from_part(note[:from], :basses)
43
+ return
44
+ end
45
+
46
+ @basses << [[note], calculate_duration(duration)]
47
+ end
48
+
49
+ def melody note = nil, duration = 1, options = {}
50
+ if note.is_a?(Hash) && note[:from]
51
+ copy_from_part(note[:from], :melodies)
52
+ return
53
+ end
54
+
55
+ @melodies << [[note], calculate_duration(duration)]
56
+ end
57
+
58
+ private
59
+
60
+ def calculate_duration(duration)
61
+ 1.to_f / ((@song.tempo.to_f/120) * duration/2)
62
+ end
63
+
64
+ def copy_from_part(name, type)
65
+ part = @song.get_part(name)
66
+ instance_variable_set(:"@#{type}", part.send(type))
67
+ end
68
+ end
69
+ end
data/lib/niki/song.rb ADDED
@@ -0,0 +1,81 @@
1
+ module Niki
2
+ class Song
3
+ attr_reader :tempo, :drum_notes
4
+
5
+ include Niki::Chords
6
+
7
+ def initialize(options, &block)
8
+ @clock = Archaeopteryx::Midi::Clock.new(options[:tempo] || 127)
9
+
10
+ @midi = Archaeopteryx::Midi::PracticalRubyProjects::LiveMIDI.new(:clock => @clock )
11
+
12
+ @parts = []
13
+ @channel = {}
14
+ @drum_notes = {}
15
+ @tempo = options[:tempo]
16
+ self.instance_eval &block
17
+ end
18
+
19
+ def channel(instrument, number)
20
+ @channel[instrument] = number - 1
21
+ end
22
+
23
+ def part(name, &block)
24
+ raise "#{name} is already defined!" if has_part?(name)
25
+ @parts << Part.new(name, self, &block)
26
+ end
27
+
28
+ def configure(instrument, &block)
29
+ case instrument
30
+ when :drums
31
+ block.call @drum_notes
32
+ end
33
+ end
34
+
35
+ def repeat(name, options = {})
36
+ (options[:times] || 1).times do
37
+ @parts << get_part(name)
38
+ end
39
+ end
40
+
41
+ def play
42
+ @parts.each do |part|
43
+ [:basses, :chords, :melodies, :drums].map do |instrument_name|
44
+ Thread.start do
45
+ play_part(part, instrument_name)
46
+ end
47
+ end.map { |thread| thread.join }
48
+ end
49
+ end
50
+
51
+ def play_part(part, instrument_name)
52
+ part.send(instrument_name).each do |instrument|
53
+ notes = [instrument.first].flatten.compact.map do |note|
54
+ Archaeopteryx::Midi::Note.create(
55
+ :channel => @channel[instrument_name],
56
+ :number => note,
57
+ :duration => instrument.last,
58
+ :velocity => 127)
59
+ end
60
+ notes.each do |note|
61
+ @midi.note_on(note)
62
+ end
63
+ sleep(instrument.last)
64
+ notes.each do |note|
65
+ @midi.note_off(note)
66
+ end
67
+ end
68
+ end
69
+
70
+ def get_part(name)
71
+ @parts.detect {|p| p.name == name }
72
+ end
73
+
74
+ private
75
+
76
+ def has_part?(name)
77
+ !!@parts.map(&:name).detect {|part_name| part_name == name }
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,3 @@
1
+ module Niki
2
+ VERSION = "0.0.1"
3
+ end
data/niki.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "niki/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "niki"
7
+ s.version = Niki::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Josep M. Bach"]
10
+ s.email = ["josep.m.bach@gmail.com"]
11
+ s.homepage = "http://github.com/txus/niki"
12
+ s.summary = %q{A DSL to describe and play structured musical pieces, i.e. songs}
13
+ s.description = %q{A DSL to describe and play structured musical pieces, i.e. songs}
14
+
15
+ s.rubyforge_project = "niki"
16
+
17
+ s.add_runtime_dependency 'midilib'
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib", "vendor"]
23
+ end
@@ -0,0 +1,28 @@
1
+ alias :L :lambda
2
+
3
+ %w{rubygems midilib/sequence midilib/consts}.each {|lib| require lib}
4
+
5
+ %w{archaeopteryx/core_ext/struct
6
+ archaeopteryx/core_ext/array
7
+
8
+ archaeopteryx/loop
9
+ archaeopteryx/drum
10
+ archaeopteryx/rhythm
11
+ archaeopteryx/mix
12
+
13
+ archaeopteryx/live_hacks
14
+ archaeopteryx/track
15
+ archaeopteryx/clip
16
+
17
+ archaeopteryx/midi/note
18
+ archaeopteryx/midi/clock
19
+
20
+ archaeopteryx/midi/file_output/file_midi
21
+
22
+ archaeopteryx/midi/practical_ruby_projects/no_midi_destinations
23
+ archaeopteryx/midi/practical_ruby_projects/core_midi
24
+ archaeopteryx/midi/practical_ruby_projects/core_foundation
25
+ archaeopteryx/midi/practical_ruby_projects/live_midi
26
+ archaeopteryx/midi/practical_ruby_projects/timer}.each do |archaeopteryx|
27
+ require File.expand_path(File.dirname(__FILE__)) + "/../#{archaeopteryx}"
28
+ end
@@ -0,0 +1,37 @@
1
+ module Archaeopteryx
2
+ class Clip
3
+ attr_accessor :measures
4
+ def initialize(attributes)
5
+ @message = Message.create(attributes.merge(:midi_channel => 0,
6
+ :value => 127))
7
+ @measures = attributes[:measures]
8
+ end
9
+ def notes(measure)
10
+ []
11
+ end
12
+ def messages(measure)
13
+ 0 == measure % @measures ? [@message] : []
14
+ # if 0 == @relative_measure % @measures
15
+ # [@message]
16
+ # else
17
+ # []
18
+ # @relative_measure += 1
19
+ # end
20
+ end
21
+ def mutate(measure)
22
+ end
23
+ # def choose
24
+ # relative_measure = 1
25
+ # etc.
26
+ # end
27
+ def complete?
28
+ [true, false][rand(2)]
29
+ # current_measure == final_measure
30
+ # BUT! these need to be relative measures in this Clip, not absolute measures in the overall Loop
31
+ # possible that Loop should be called Engine or some crazy shit
32
+ # I guess relative_measure is really @current_measure
33
+ end
34
+ end
35
+ end
36
+
37
+ # probably rename this to make it drum-specific
@@ -0,0 +1,11 @@
1
+ class Array
2
+ def random
3
+ self[rand(self.size)]
4
+ end
5
+ end
6
+
7
+ # the weird thing is, sometimes this method works and sometimes it doesn't. it happens with other
8
+ # libs as well as this one. wtf is going on there? gotta investigate that shit.
9
+
10
+ # oh fuck I think I know why. I require ActiveSupport in my IRB so I'm assuming Array#rand from
11
+ # AS is part of Ruby itself. ooops.
@@ -0,0 +1,7 @@
1
+ # this allows you to create Structs and Struct subclasses with pseudo-keyword options hashes
2
+ # rather than with long sequences of unnamed args. written by Michael Fellinger.
3
+ class Struct
4
+ def self.create(hash)
5
+ new(*hash.values_at(*members.map{|member| member.to_sym}))
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ module Archaeopteryx
2
+ class Drum
3
+ attr_accessor :note, :probabilities, :when, :next, :number_generator
4
+ def initialize(attributes)
5
+ %w{note probabilities when next number_generator}.each do |attribute|
6
+ eval("@#{attribute} = attributes[:#{attribute}]")
7
+ end
8
+ @queue = [attributes[:when]]
9
+ generate
10
+ end
11
+ def play?(beat)
12
+ @when[beat]
13
+ end
14
+ def generate
15
+ beats_on_which_to_play = []
16
+ @probabilities.each_with_index do |probability, index|
17
+ beats_on_which_to_play << index if @number_generator[] <= probability
18
+ end
19
+ @queue << L{|beat| beats_on_which_to_play.include? beat}
20
+ @when = @next[@queue]
21
+ end
22
+ alias :mutate :generate
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ class Message < Struct.new(:midi_channel, :controller_number, :value)
2
+ end
3
+
4
+ class TapTempo
5
+ def midi_channel; 15 ; end
6
+ def controller_number ; 7 ; end
7
+ def value ; 127 ; end
8
+ end
@@ -0,0 +1,32 @@
1
+ module Archaeopteryx
2
+ class Loop
3
+ attr_reader :midi
4
+ def initialize(attributes)
5
+ %w{generator
6
+ measures
7
+ beats
8
+ clock
9
+ midi
10
+ evil_timer_offset_wtf
11
+ infinite}.each do |option|
12
+ eval("@#{option} = attributes[:#{option}]")
13
+ end
14
+ end
15
+ def generate_beats
16
+ (1..@measures).each do |measure|
17
+ @generator.mutate(measure)
18
+ (0..(@beats - 1)).each do |beat|
19
+ play @generator.notes(beat)
20
+ @clock.tick
21
+ end
22
+ end
23
+ if @midi.infinite?
24
+ @midi.timer.at((@clock.start + @clock.time) - @evil_timer_offset_wtf,
25
+ &(L{generate_beats}))
26
+ end
27
+ end
28
+ def play(music)
29
+ music.each {|note| @midi.play(note)}
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ # get singleton from std lib
2
+ module Archaeopteryx
3
+ module Midi
4
+ class Clock
5
+ attr_reader :time, :interval, :start
6
+ def initialize(bpm)
7
+ # assumes 16-step step sequencer, 4/4 beat, etc.
8
+ self.bpm = bpm
9
+ @start = Time.now.to_f
10
+ @time = 0
11
+ end
12
+ def bpm=(bpm)
13
+ seconds_in_a_minute = 60.0
14
+ beats_in_a_measure = 4.0
15
+ @interval = seconds_in_a_minute / bpm.to_f / beats_in_a_measure
16
+ end
17
+ def tick
18
+ @time += @interval
19
+ @time
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,44 @@
1
+ # a quick note about FAIL. FileMIDI is actually of the same unspecified superclass as LiveMIDI. both
2
+ # need new, clearer names, but the nonexistent abstract superclass is just a class which takes options
3
+ # for args and has a method called play() which takes a note argument.
4
+
5
+ include MIDI
6
+
7
+ # christ this should probably live in some kind of module or something
8
+
9
+ class FileMIDI
10
+ attr_accessor :filename
11
+ def initialize(options)
12
+ raise :hell unless options.is_a? Hash
13
+ @filename = options[:filename]
14
+ @clock = options[:clock]
15
+ @events = []
16
+
17
+ @sequence = MIDI::Sequence.new
18
+ @sequence.tracks << (@track = MIDI::Track.new(@sequence))
19
+ @track.events << Tempo.new(Tempo.bpm_to_mpq(options[:tempo])) if options[:tempo]
20
+ @track.events << MetaEvent.new(META_SEQ_NAME, options[:name]) if options[:name]
21
+
22
+ # I'm not sure if this is actually necessary (?)
23
+ @track.events << Controller.new(0, CC_VOLUME, 127)
24
+ @track.events << ProgramChange.new(0, 1, 0)
25
+ end
26
+ def infinite?
27
+ false
28
+ end
29
+ def midilib_delta
30
+ # figuring this shit out was an epic fucking nightmare and to be honest I still have no idea why it works
31
+ ((@sequence.note_to_delta("16th") / @clock.interval) * @clock.time).to_i
32
+ end
33
+ def play(note)
34
+ @track.merge [NoteOnEvent.new(note.channel,
35
+ note.number,
36
+ note.velocity,
37
+ midilib_delta)]
38
+ end
39
+ def write
40
+ File.open(@filename, 'wb') do |file|
41
+ @sequence.write(file)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,9 @@
1
+ module Archaeopteryx
2
+ module Midi
3
+ class Note < Struct.new(:channel, :number, :duration, :velocity)
4
+ def to_code
5
+ "Note.new(#{self.channel}, #{self.number}, #{self.duration}, #{self.velocity})"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module CoreFoundation
2
+ require 'dl/import'
3
+ extend DL::Importable
4
+ dlload '/System/Library/Frameworks/CoreFoundation.framework/Versions/Current/CoreFoundation'
5
+
6
+ extern "void * CFStringCreateWithCString (void *, char *, int)"
7
+ end
@@ -0,0 +1,16 @@
1
+ module CoreMIDI
2
+ require 'dl/import'
3
+ extend DL::Importable
4
+
5
+ dlload '/System/Library/Frameworks/CoreMIDI.framework/Versions/Current/CoreMIDI'
6
+
7
+ extern "int MIDIClientCreate(void *, void *, void *, void *)"
8
+ extern "int MIDIClientDispose(void *)"
9
+ extern "int MIDIGetNumberOfDestinations()"
10
+ extern "void * MIDIGetDestination(int)"
11
+ extern "int MIDIOutputPortCreate(void *, void *, void *)"
12
+ extern "void * MIDIPacketListInit(void *)"
13
+ # http://groups.google.com/group/ruby-midi/browse_thread/thread/85de6ea9373c57a4
14
+ extern "void * MIDIPacketListAdd(void *, int, void *, int, int, void*)"
15
+ extern "int MIDISend(void *, void *, void *)"
16
+ end
@@ -0,0 +1,117 @@
1
+ module Archaeopteryx
2
+ module Midi
3
+ module PracticalRubyProjects
4
+ class LiveMIDI
5
+
6
+ # this object is a gateway between the Objective-C extern-ed functions from the CoreMIDI API
7
+ # and good old-fashioned Ruby. As such some of the code gets weird. This is nearly all from
8
+ # Topher Cyll's wicked book referenced in the MIT license file (practical_ruby_projects.rb),
9
+ # but with some refactoring and modification.
10
+
11
+ include CoreMIDI
12
+ include CoreFoundation
13
+
14
+ attr_reader :interval # this is a totally misleading variable name! real interval lives on Clock
15
+ attr_reader :timer
16
+ ON = 0x90
17
+ OFF = 0x80
18
+ PC = 0xC0 # program change, I think; not actually useable in Propellerhead Reason v3
19
+ CONTROLLER = 0xB0 # arbitrary controller message
20
+
21
+ def infinite?
22
+ true
23
+ end
24
+
25
+ def to_code
26
+ "LiveMIDI.new(:clock => @clock = attributes[:clock], :logging => false)"
27
+ end
28
+
29
+ def initialize(options)
30
+ @clock = options[:clock]
31
+ @logging = options[:logging]
32
+ @midi_destination = options[:midi_destination] || 0
33
+ if @logging
34
+ puts <<LOG_PLAYBACK
35
+ require 'lib/archaeopteryx'
36
+ @midi = #{self.to_code}
37
+ LOG_PLAYBACK
38
+ end
39
+ @interval = 60.0/120 # this is just a polling interval for the Thread - not a musical one
40
+ @timer = Timer.new(@interval/1000)
41
+ open
42
+ end
43
+
44
+ def play(midi_note, on_time = @clock.time)
45
+ if @logging
46
+ puts "@midi.play(#{midi_note.to_code}, #{on_time})"
47
+ end
48
+ on_time += @clock.start
49
+ @timer.at(on_time) {note_on(midi_note)}
50
+ @timer.at(on_time + midi_note.duration) {note_off(midi_note)}
51
+ end
52
+
53
+ def send(message)
54
+ send_controller_message(message.midi_channel, message.controller_number, message.value)
55
+ end
56
+ def send_controller_message(midi_channel, controller_number, value, on_time = @clock.time)
57
+ on_time += @clock.start
58
+ puts "scheduling #{controller_number} for #{on_time}" if @logging
59
+ @timer.at(on_time) do
60
+ control(midi_channel, controller_number, value)
61
+ end
62
+ end
63
+
64
+ def open
65
+ client_name = CoreFoundation.cFStringCreateWithCString(nil, "RubyMIDI", 0)
66
+ @client = DL::PtrData.new(nil)
67
+ CoreMIDI.mIDIClientCreate(client_name, nil, nil, @client.ref)
68
+
69
+ port_name = CoreFoundation.cFStringCreateWithCString(nil, "Output", 0)
70
+ @outport = DL::PtrData.new(nil)
71
+ CoreMIDI.mIDIOutputPortCreate(@client, port_name, @outport.ref)
72
+
73
+ number_of_destinations = CoreMIDI.mIDIGetNumberOfDestinations()
74
+ raise NoMIDIDestinations if number_of_destinations < 1
75
+ @destination = CoreMIDI.mIDIGetDestination(@midi_destination)
76
+ end
77
+
78
+ def close
79
+ CoreMIDI.mIDIClientDispose(@client)
80
+ end
81
+
82
+ def clear
83
+ @timer.flush
84
+ end
85
+
86
+ def message(*args)
87
+ format = "C" * args.size
88
+ bytes = args.pack(format).to_ptr
89
+ packet_list = DL.malloc(256)
90
+ packet_ptr = CoreMIDI.mIDIPacketListInit(packet_list)
91
+ # http://groups.google.com/group/ruby-midi/browse_thread/thread/85de6ea9373c57a4
92
+ packet_ptr = CoreMIDI.mIDIPacketListAdd(packet_list, 256, packet_ptr, 0, args.size, bytes)
93
+ CoreMIDI.mIDISend(@outport, @destination, packet_list)
94
+ end
95
+
96
+ def note_on(midi_note)
97
+ message(ON | midi_note.channel, midi_note.number, midi_note.velocity)
98
+ end
99
+
100
+ def note_off(midi_note)
101
+ message(OFF | midi_note.channel, midi_note.number, midi_note.velocity)
102
+ end
103
+
104
+ def program_change(channel, preset)
105
+ message(PC | channel, preset)
106
+ end
107
+
108
+ def pulse(channel, controller_id, value)
109
+ # puts "sending now: #{Time.now.to_f}" if @logging
110
+ # puts "#{[channel, controller_id, value].inspect}" if @logging
111
+ message(CONTROLLER | channel, controller_id, value)
112
+ end
113
+ alias :control :pulse
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,8 @@
1
+ module Archaeopteryx
2
+ module Midi
3
+ module PracticalRubyProjects
4
+ class NoMIDIDestinations < Exception
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,30 @@
1
+ module Archaeopteryx
2
+ module Midi
3
+ # most of this code comes from "Practical Ruby Projects" by Topher Cyll, published by
4
+ # APress, which is a pretty bad-ass book. here's the license:
5
+ #
6
+ # The MIT License
7
+ #
8
+ # Copyright (c) 2006, 2007 Topher Cyll
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ # of this software and associated documentation files (the "Software"), to deal
12
+ # in the Software without restriction, including without limitation the rights
13
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ # copies of the Software, and to permit persons to whom the Software is
15
+ # furnished to do so, subject to the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be included in
18
+ # all copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
+ # THE SOFTWARE.
27
+ module PracticalRubyProjects
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ module Archaeopteryx
2
+ module Midi
3
+ module PracticalRubyProjects
4
+ class Timer
5
+ def initialize(resolution)
6
+ @resolution = resolution
7
+ @queue = []
8
+
9
+ Thread.new do
10
+ while true
11
+ dispatch
12
+ sleep(@resolution)
13
+ end
14
+ end
15
+ end
16
+
17
+ def flush
18
+ @queue = []
19
+ end
20
+
21
+ def at(time, &block)
22
+ time = time.to_f if time.kind_of?(Time)
23
+ @queue.push [time, block]
24
+ end
25
+
26
+ private
27
+ def dispatch
28
+ now = Time.now.to_f
29
+ ready, @queue = @queue.partition {|time, proc| time <= now}
30
+ ready.each {|time, proc| proc.call(time)}
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ module Archaeopteryx
2
+ class Mix
3
+ attr_accessor :rhythms
4
+ def initialize(attributes)
5
+ @rhythms = attributes[:rhythms]
6
+ end
7
+ def notes(beat)
8
+ notes = []
9
+ @rhythms.each do |rhythm|
10
+ notes << rhythm.notes(beat)
11
+ end
12
+ notes.flatten
13
+ end
14
+ def messages(measure) # obviously these should be refactored to inject()s
15
+ messages = []
16
+ @rhythms.each do |rhythm|
17
+ messages << rhythm.messages(measure)
18
+ end
19
+ messages.flatten
20
+ end
21
+ def mutate(measure)
22
+ @rhythms.each {|rhythm| rhythm.mutate(measure)}
23
+ end
24
+ end
25
+ end
26
+
27
+ # Mix nearly identical to Rhythm
@@ -0,0 +1,29 @@
1
+ module Archaeopteryx
2
+ class Rhythm
3
+ def initialize(attributes)
4
+ @mutation = attributes[:mutation]
5
+ @drumfile = attributes[:drumfile]
6
+ reload
7
+ end
8
+ def reload
9
+ puts "\a" # flash the screen ; only valid on my box and similarly configured machines!
10
+ @drums = eval(File.read(@drumfile))
11
+ end
12
+ def notes(beat)
13
+ (@drums.collect do |drum|
14
+ drum.note if drum.play? beat
15
+ end).compact
16
+ end
17
+ def messages(beat)
18
+ []
19
+ end
20
+ def mutate(measure)
21
+ if @mutation[measure]
22
+ reload # reloading can kill mutations!
23
+ @drums.each {|drum| drum.mutate}
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ # probably rename this to make it drum-specific
@@ -0,0 +1,16 @@
1
+ module Archaeopteryx
2
+ class Track < Mix
3
+ attr_accessor :current
4
+ def initialize(attributes)
5
+ @rhythms = attributes[:rhythms]
6
+ @measure = 1
7
+ new_clip
8
+ end
9
+ def new_clip
10
+ @current = @rhythms.random
11
+ end
12
+ def messages(measure)
13
+ @current.messages(measure)
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: niki
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Josep M. Bach
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-08-15 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: midilib
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ description: A DSL to describe and play structured musical pieces, i.e. songs
35
+ email:
36
+ - josep.m.bach@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - .gitignore
45
+ - Gemfile
46
+ - Gemfile.lock
47
+ - Rakefile
48
+ - Readme.md
49
+ - examples/my_song.rb
50
+ - examples/my_song.rns
51
+ - lib/core_ext/array.rb
52
+ - lib/core_ext/fixnum.rb
53
+ - lib/niki.rb
54
+ - lib/niki/chords.rb
55
+ - lib/niki/part.rb
56
+ - lib/niki/song.rb
57
+ - lib/niki/version.rb
58
+ - niki.gemspec
59
+ - vendor/archaeopteryx/archaeopteryx.rb
60
+ - vendor/archaeopteryx/clip.rb
61
+ - vendor/archaeopteryx/core_ext/array.rb
62
+ - vendor/archaeopteryx/core_ext/struct.rb
63
+ - vendor/archaeopteryx/drum.rb
64
+ - vendor/archaeopteryx/live_hacks.rb
65
+ - vendor/archaeopteryx/loop.rb
66
+ - vendor/archaeopteryx/midi/clock.rb
67
+ - vendor/archaeopteryx/midi/file_output/file_midi.rb
68
+ - vendor/archaeopteryx/midi/note.rb
69
+ - vendor/archaeopteryx/midi/practical_ruby_projects/core_foundation.rb
70
+ - vendor/archaeopteryx/midi/practical_ruby_projects/core_midi.rb
71
+ - vendor/archaeopteryx/midi/practical_ruby_projects/live_midi.rb
72
+ - vendor/archaeopteryx/midi/practical_ruby_projects/no_midi_destinations.rb
73
+ - vendor/archaeopteryx/midi/practical_ruby_projects/practical_ruby_projects.rb
74
+ - vendor/archaeopteryx/midi/practical_ruby_projects/timer.rb
75
+ - vendor/archaeopteryx/mix.rb
76
+ - vendor/archaeopteryx/rhythm.rb
77
+ - vendor/archaeopteryx/track.rb
78
+ homepage: http://github.com/txus/niki
79
+ licenses: []
80
+
81
+ post_install_message:
82
+ rdoc_options: []
83
+
84
+ require_paths:
85
+ - lib
86
+ - vendor
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ hash: 3
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ hash: 3
102
+ segments:
103
+ - 0
104
+ version: "0"
105
+ requirements: []
106
+
107
+ rubyforge_project: niki
108
+ rubygems_version: 1.8.6
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: A DSL to describe and play structured musical pieces, i.e. songs
112
+ test_files: []
113
+