Class: GameListener

Inherits:
Object
  • Object
show all
Defined in:
lib/bangkok/gamelistener.rb

Overview

The pieces and board call methods on an instance of GameListener, which turns those events into MIDI events.

Constant Summary collapse

PIECE_NAMES =
{
  :P => "Pawn",
  :R => "Rook",
  :N => "Night",
  :B => "Bishop",
  :Q => "Queen",
  :K => "King"
}
RANK_TO_NOTE =

Build notes to play for ranks

{}
PIECE_MIDI_INFO =
{}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config_file_path = nil) ⇒ GameListener

Returns a new instance of GameListener.



58
59
60
# File 'lib/bangkok/gamelistener.rb', line 58

def initialize(config_file_path=nil)
  read_config(config_file_path) if config_file_path
end

Instance Attribute Details

#seqObject (readonly)

MIDI::Sequence



56
57
58
# File 'lib/bangkok/gamelistener.rb', line 56

def seq
  @seq
end

Class Method Details

.black(piece_sym, program_change) ⇒ Object

Used by config files to set the program change value for a piece.



37
38
39
# File 'lib/bangkok/gamelistener.rb', line 37

def GameListener.black(piece_sym, program_change)
  GameListener::PIECE_MIDI_INFO[:black][piece_sym] = program_change
end

.white(piece_sym, program_change) ⇒ Object

Used by config files to set the program change value for a piece.



32
33
34
# File 'lib/bangkok/gamelistener.rb', line 32

def GameListener.white(piece_sym, program_change)
  PIECE_MIDI_INFO[:white][piece_sym] = program_change
end

Instance Method Details

#capture(attacker, loser) ⇒ Object



147
148
# File 'lib/bangkok/gamelistener.rb', line 147

def capture(attacker, loser)
end

#channel_of(piece) ⇒ Object



75
76
77
78
# File 'lib/bangkok/gamelistener.rb', line 75

def channel_of(piece)
  [:white, :black].index(piece.color) * 8 +
    [:P, :R, :N, :B, :Q, :K].index(piece.piece)
end

#checkObject



150
151
# File 'lib/bangkok/gamelistener.rb', line 150

def check
end

#checkmateObject



153
154
# File 'lib/bangkok/gamelistener.rb', line 153

def checkmate
end

#create_tracksObject

End of listener interface

++



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/bangkok/gamelistener.rb', line 165

def create_tracks
  [:white, :black].each_with_index { | color, chan_base_offset |
    [:P, :R, :N, :B, :Q, :K].each_with_index { | piece_sym, chan_offset |
      track = Track.new(@seq)
      @seq.tracks << track

      track.name = "#{color.to_s.capitalize} #{PIECE_NAMES[piece_sym]}"

      program_num = PIECE_MIDI_INFO[color][piece_sym]
      track.instrument = GM_PATCH_NAMES[program_num]
      track.events << ProgramChange.new(chan_base_offset * 8 + chan_offset,
                                        program_num)
    }
  }
end

#end_gameObject



102
103
104
105
106
107
108
# File 'lib/bangkok/gamelistener.rb', line 102

def end_game
  # When we created events, we set their start times, not their delta times.
  # Now is the time to fix that. Sort sorts by start times then calls
  # recalc_delta_from_times.
  @seq.tracks.each { | t | t.sort }
  @seq.write(@io)
end

#file_to_pan(file) ⇒ Object

Translates a (possibly fractional) file into an integer CC_PAN value.



219
220
221
# File 'lib/bangkok/gamelistener.rb', line 219

def file_to_pan(file)
  return interpolate(0, 127, 0, 7, file).to_i
end

#generate_notes(track, channel, total_delta, piece, from, to) ⇒ Object

Generate two notes, one at the current start time that is only a 32nd note long. The next follows immediately, and is for the to square. Its length is the remaining time.



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/bangkok/gamelistener.rb', line 196

def generate_notes(track, channel, total_delta, piece, from, to)
  # First note is quickly followed by the second note
  note = RANK_TO_NOTE[piece.color][from.rank]
  e = NoteOnEvent.new(channel, note, 127)
  e.time_from_start = @time_from_start
  track.events << e

  e = NoteOffEvent.new(channel, note, 127)
  e.time_from_start = @time_from_start + @first_note_delta - 1
  track.events << e

  # Second note
  note = RANK_TO_NOTE[piece.color][to.rank]
  e = NoteOnEvent.new(channel, note, 127)
  e.time_from_start = @time_from_start + @first_note_delta
  track.events << e

  e = NoteOffEvent.new(channel, note, 127)
  e.time_from_start = @time_from_start + total_delta - 1
  track.events << e
end

#generate_pan(track, channel, delta, value) ⇒ Object

Generates a single pan event. #move_to calls this multiple times, passing in new delta and value values.



240
241
242
243
244
245
# File 'lib/bangkok/gamelistener.rb', line 240

def generate_pan(track, channel, delta, value)
  raise "pan: bogus value #{value}" unless value >= 0 && value <= 127
  e = Controller.new(channel, CC_PAN, value)
  e.time_from_start = @time_from_start + delta
  track.events << e
end

#generate_portamento(track, channel, total_delta) ⇒ Object

Generates a single portamento event.



248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/bangkok/gamelistener.rb', line 248

def generate_portamento(track, channel, total_delta)
  e = Controller.new(channel, CC_PORTAMENTO_TIME, 0)
  e.time_from_start = @time_from_start
  track.events << e

  value = interpolate(32, 100, 0, @max_delta, total_delta).to_i
  raise "portamento: bogus value #{value}" unless value >= 0 && value <= 127
  e = Controller.new(channel, CC_PORTAMENTO_TIME, value)
       
  e.time_from_start = @time_from_start + @portamento_start
  track.events << e
end

#generate_volume(track, channel, delta, value) ⇒ Object

Generates a single volume event. #move_to calls this multiple times, passing in new delta and value values.



231
232
233
234
235
236
# File 'lib/bangkok/gamelistener.rb', line 231

def generate_volume(track, channel, delta, value)
  raise "volume: bogus value #{value}" unless value >= 0 && value <= 127
  e = Controller.new(channel, CC_VOLUME, value)
  e.time_from_start = @time_from_start + delta
  track.events << e
end

#interpolate(range_min, range_max, value_min, value_max, value) ⇒ Object

Returns a value between range_min and range_max inclusive that is proportional to value‘s place between value_min and value_max. The returned value may be floating point. If range_min and range_max or value_min and value_max are out of order, they are swapped.



185
186
187
188
189
190
191
# File 'lib/bangkok/gamelistener.rb', line 185

def interpolate(range_min, range_max, value_min, value_max, value)
  range_min, range_max = range_max, range_min if range_min > range_max
  value_min, value_max = value_max, value_min if value_min > value_max
  return range_min if value == value_min
  frac = (value_max.to_f - value_min.to_f) / (value.to_f - value_min.to_f)
  return range_min + (range_max.to_f - range_min.to_f) / frac
end

#move(piece, from, to) ⇒ Object

Move piece from one space to another. Generate two notes and sets CC_PORTAMENTO_TIME so there is a glide from the first note to the second. Also output multiple volume and pan values, moving smoothly from the original to the new value.



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
# File 'lib/bangkok/gamelistener.rb', line 114

def move(piece, from, to)
  raise "from #{from} may not be off the board" unless from.on_board?

  # Do nothing if the piece moves off the board, because either capture()
  # or pawn_to_queen() will be called.
  return unless to.on_board?

  track = track_of(piece)
  channel = channel_of(piece)

  dist = from.distance_to(to)
  total_delta = @seq.length_to_delta(dist) # quarter note per space

  generate_notes(track, channel, total_delta, piece, from, to)
  generate_portamento(track, channel, total_delta)

  steps = interpolate(16, 64, 0, @max_delta, total_delta).to_i
  delta = (total_delta / steps).to_i
  start = @time_from_start

  steps.times { | step |
    val = rank_to_volume(interpolate(from.rank, to.rank, 0, steps-1, step))
    generate_volume(track, channel, start, val)
    
    val = file_to_pan(interpolate(from.file, to.file, 0, steps-1, step))
    generate_pan(track, channel, start, val)

    start += delta
  }

  @time_from_start += total_delta
end

#pawn_to_queen(pawn) ⇒ Object



156
157
# File 'lib/bangkok/gamelistener.rb', line 156

def pawn_to_queen(pawn)
end

#rank_to_volume(rank) ⇒ Object

Translates a (possibly fractional) rank into an integer CC_VOLUME value.



224
225
226
227
# File 'lib/bangkok/gamelistener.rb', line 224

def rank_to_volume(rank)
  rank = 3.5 - (3.5 - rank).abs
  return interpolate(10, 127, 0, 3.5, rank).to_i
end

#read_config(config_file_path) ⇒ Object



62
63
64
65
66
67
# File 'lib/bangkok/gamelistener.rb', line 62

def read_config(config_file_path)
  IO.readlines(config_file_path).each { | line |
    line.chomp!
    class_eval(line)
  }
end

#start_game(io) ⇒ Object

Listener interface

++



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/bangkok/gamelistener.rb', line 86

def start_game(io)
  @io = io
  @seq = Sequence.new
  track = Track.new(@seq)     # Tempo track
  track.name = "Tempo track"
  @seq.tracks << track

  @first_note_delta = @seq.note_to_delta('32nd')
  @portamento_start = @seq.note_to_delta('64th')
  @max_delta =
	@seq.length_to_delta(Square.at(0, 0).distance_to(Square.at(7, 7)))

  create_tracks()
  @time_from_start = 0
end

#track_of(piece) ⇒ Object



69
70
71
72
73
# File 'lib/bangkok/gamelistener.rb', line 69

def track_of(piece)
  i = [:white, :black].index(piece.color) * 6 +
    [:P, :R, :N, :B, :Q, :K].index(piece.piece)
  @seq.tracks[i + 1]          # 0'th track is temp track
end