Sonic Pi and Novation Launchpad

I got a Novation Launchpad Mini recently, which are really really fun and affordable.

I’ll post some tips here how to use it. Lets start with a simple text scrolling example:

Source code:

# Midi in port for the launchpad
launchpad_in = "/midi:midiin2_(lpminimk3_midi)_1:1/*"

# Set novation mini to programmer mode
define :set_programmer_mode do
  midi_sysex 0xf0, 0x00, 0x20, 0x29, 0x02, 0x0D, 0x0E, 0x01, 0xf7
end

# Light up multiple leds from novation launchpad
define :led_sysex do |values|
  midi_sysex 0xf0, 0x00, 0x20, 0x29, 0x02, 0x0d, 0x03, *values, 0xf7, port: launchpad_out
end

define :stop_text do
  midi_sysex 0xf0, 0x00, 0x20, 0x29, 0x02, 0x0d, 0x07, 0xf7
end

define :scroll_text do |text, loop=0x01,speed=0x07,rgb=[255,255,255]|
  text = text.chars.map { |b| b.ord }
  midi_sysex 0xf0, 0x00, 0x20, 0x29, 0x02, 0x0d, 0x07, loop, speed, 0x01, *rgb.map {|color| ((127*color)/255) }, *text, 0xf7
end

set_programmer_mode

scroll_text 'Sonic Pi  \^.^/', 1, 15, [255,20,147]

6 Likes

I really don’t know why I am doing this in the middle of the night, but here it is: Minesweeper for Sonic Pi. This example shows how to send and receive midi with Novation launchpad MK3. There is a programmers reference that helps out figuring how the messaging works. These examples are only tested with MK3 but could work on older devices.

This video does not really capture the lights properly because of the light exposure, but I hope it shows the basic idea. Found mine at the first try - how typical :smile:

Source for the game:

# Sonic Mines - Minesweeper for Sonic Pi
# Created for Novation Launchpad Mini Mk3

use_debug false
use_midi_logging false

real_random = SecureRandom.random_number(10000000000000000000)
use_random_seed = real_random

# Midi ports for the launchpad
launchpad_in = "/midi:midiin2_(lpminimk3_midi)_1:1/*"
launchpad_out = "midiout2_(lpminimk3_midi)_2"

midi_clock_beat 0.5, port: launchpad_out

# Set novation mini to programmer mode
define :set_programmer_mode do
  midi_sysex 0xf0, 0x00, 0x20, 0x29, 0x02, 0x0D, 0x0E, 0x01, 0xf7
end

# Light up multiple leds from novation launchpad
define :led_sysex do |values|
  midi_sysex 0xf0, 0x00, 0x20, 0x29, 0x02, 0x0d, 0x03, *values, 0xf7, port: launchpad_out
end

# Stop scrolling text
define :stop_text do
  midi_sysex 0xf0, 0x00, 0x20, 0x29, 0x02, 0x0d, 0x07, 0xf7
end

# Helper method for defining midi rgb
# Nice color picker: https://www.rapidtables.com/web/color/html-color-codes.html
define :rgb do |r,g,b|
  [((127*r)/255),((127*g/255)),((127*b)/255)]
end

# Scroll text on novation launchpad
define :scroll_text do |text, loop=0x01,speed=0x07,rgb=[127,127,127]|
  text = text.chars.map { |b| b.ord }
  midi_sysex 0xf0, 0x00, 0x20, 0x29, 0x02, 0x0d, 0x07, loop, speed, 0x01, *rgb, *text, 0xf7
end

# Set single cell flashing
define :set_cell_flash do |x, y, c1, c2|
  cell = (x.to_s+y.to_s).to_i
  values = [0x01, cell, c1, c2]
  led_sysex values
end

# Set single cell color
define :set_cell_color do |x, y, rgb|
  cell = (x.to_s+y.to_s).to_i
  values = [0x03, cell, *rgb]
  led_sysex values
end

# Set colors for the whole matrix
define :set_pad_colors do |matrix,rgb|
  pad_colors = []
  matrix.length.times do |x|
    row = matrix[x]
    row.length.times do |y|
      cell = matrix[x][y]
      cell_color = [0x03, ((matrix.length-x).to_s+(y+1).to_s).to_i, *rgb]
      pad_colors = pad_colors+cell_color
    end
  end
  led_sysex pad_colors
end

# Creates rgb from probability based on the color scheme
define :prob_to_color do |prob|
  
  # Coloring scheme for propabilities
  colors = [
    rgb(255,0,0),
    rgb(255,0,255),
    rgb(55,55,55)
  ]
  
  index = colors.index.with_index do |col,i|
    prob <= i.to_f/(colors.length-1)
  end
  
  lower = colors[index-1]
  upper = colors[index]
  upperProb = index.to_f/(colors.length-1)
  lowerProb = (index-1).to_f/(colors.length-1)
  u = (prob - lowerProb) / (upperProb - lowerProb)
  l = 1 - u
  [(lower[0]*l + upper[0]*u).to_i, (lower[1]*l + upper[1]*u).to_i, (lower[2]*l + upper[2]*u).to_i].map {|color| ((127*color)/255) }
end

define :set_neighbor_colors do |matrix, x, y|
  n = []
  
  (x-1).upto(x+1) do |a|
    (y-1).upto(y+1) do |b|
      n.push([a,b]) if !(a==x and b==y) and matrix[a] and matrix[a][b]
    end
  end
  
  l = n.min {|a,b| matrix[a[0]][a[1]] <=> matrix[b[0]][b[1]] }
  lowest = matrix[l[0]][l[1]] if l
  
  n.each do |xy|
    prob = matrix[xy[0]][xy[1]]
    set_cell_color xy[0]+1, xy[1]+1, prob_to_color(prob) if prob
  end
  
end

# Get sync type from the midi call
define :sync_type do |address|
  v = get_event(address).to_s.split(",")[6]
  if v != nil
    return v[3..-2].split("/")[1]
  else
    return "error"
  end
end

define :explode do |x,y|
  sample :ambi_choir, attack: 1.5, decay: 3.0, beat_stretch: 4
  sample :misc_cineboom, start: 0.2
  sample :vinyl_rewind
  set_cell_flash x, y, 72, 6
end

define :evade do |x,y|
  state = get(:state)
  if state and state!=:explode
    sample :guit_harmonics, amp: 3
    sample :mehackit_robot3
    set_cell_color x, y, rgb(0,255,0)
    $game[:board][x-1][y-1] = nil # Add visit to matrix
    set_neighbor_colors $game[:board], x-1, y-1
    
  end
end

define :start_game do
  stop_text # Stop texts if running
  set_programmer_mode # Set programmer mode
  board = Array.new(8) { Array.new(8) { rand }} # Create new board
  set_pad_colors board, rgb(25,25,25) # Set color
  set :state, :relax
  chance_to_explode = 0.15
  number_of_mines = board.map{|r| r.count {|x| x<0.15 }}.inject(0,:+)
  new_game = {board: board, hits: 0, events: [], explode_prob: chance_to_explode, hits_to_win: 64-number_of_mines }
  print new_game
  new_game
end

# Start a new game
$game = start_game

# Thread for listening events from the novation launchpad
live_loop :sonicmines do
  use_real_time
  # midi note is touch position 11, 12, 13 ...
  # midi velocity is touch 127=on 0=off
  pad, touch = sync launchpad_in
  # note_on = pads, control_change = options
  type = sync_type launchpad_in
  
  xy = pad.to_s.chars
  x = xy[0].to_i
  y = xy[1].to_i
  
  if type=="note_on"
    cell_prob = $game[:board][x-1][y-1]
    if touch==0 # Touch off
      if cell_prob # Visited cell
        if cell_prob < $game[:explode_prob]
          $game[:events].push({event: :explode, x: x, y: y})
        else
          $game[:events].push({event: :evade, x: x, y: y})
        end
      end
    else # Touch on
      if cell_prob
        set_cell_flash x, y, 18, 5
        set :state, :exited
      end
    end
  elsif type=="control_change"
    if pad==19 # Start new game
      $game = start_game
    end
  end
end

# Thread for keeping up the score
live_loop :check_events do
  use_real_time
  sync :music
  
  end_game = false
  sleep 3
  
  puff = $game[:events].select {|e| e[:event]==:explode}
  puff.each do |e|
    explode e[:x], e[:y]
    end_game = true
    set :state, :explode
  end
  
  yiehaa = $game[:events].select {|e| e[:event]==:evade}
  yiehaa.each do |e|
    evade e[:x], e[:y]
    $game[:hits]+=1
    print "Hits remaining: "+($game[:hits_to_win]-$game[:hits]).to_s
  end
  
  if end_game
    sleep 1.0
    scroll_text "BOOM! + + + Try again from STOP [o_o]", 1, 15, rgb(178,34,34)
    explode = false
  elsif $game[:hits]>=$game[:hits_to_win]
    set :state, :happy
    sleep 3
    scroll_text "WINNER! \^.^/ <3 <3 <3 Press STOP to try again! * * * ", 1, 15, rgb(255,255,0)
  else
    set :state, :relax
  end
  $game[:events] = []
  
end

exited = (ring 75,76,77,76)
relax = (scale :a3, :gong).shuffle
happy = (scale :a4, :major_pentatonic).shuffle
sad = (scale :a3, :acem_asiran).shuffle

# Thread for creating exiting music from the game state
live_loop :music do
  state = get(:state)
  tick
  synth :dull_bell, note: exited.look if state==:exited
  synth :pretty_bell, amp: 0.5, note: relax.look if state==:relax
  synth :chiplead, note: happy.look if state==:happy
  synth :dark_ambience, note: sad.look if state==:explode
  sample :drum_heavy_kick if spread(1,4).look
  sample :drum_tom_hi_soft, amp: 0.5 if spread(4,23).rotate(1).look
  sample :glitch_perc3, amp: 0.5 if spread(1,36).rotate(-6).look
  sample :elec_pop, amp: 0.5 if spread(1, 16).rotate(3).look
  sleep 0.25
  if rand>0.5
    relax = relax.shuffle
    happy = happy.shuffle
    sad = sad.shuffle
  end
end

EDIT: Added colours to neighbor cells based on propability

4 Likes

Made few changes today which makes the gameplay more fun. Used seed is randomized using Securerandom, so that the placement and the number of the mines is actually random. Pads are now colored based on the probabilities to hit a mine next to a free spot. Color schemes can also be edited easily to suit your personal taste or range in color sight. To win the game you also have to sweep all the mines like in the original game. Here’s also a better video with my kid trying to figure out how to beat the game :slight_smile:

Now it’s also a family game as it supports simultanious playing. Sometimes simultanious events still causes some problems time to time, like music doesnt play at the right time. I’m not yet sure how to best deal with multiple threads and the shared states between MIDI listener thread and separate game threads.

1 Like

Well this escalated quickly to quite complex things … maybe next I’ll post how to make some simple drum etc. pads what this thing is actually made for … but that’s not really why I bought it :slight_smile:

Next I could also start coding anything from sonified Memory game, Tic Tac Toe, Snake or some sort of Chess. So many options and so little time :scream:

Oh, that’s great! :slight_smile:
Time to spend money on these lovely controllers…

Oh yeah. Nice toys. I consulted my kids again and they wanted this:

… But there’s still some bugs so code comes later :slight_smile:

2 Likes

Made a github project for Novation games for these games, including Tic Tac Toe. It also now has parameters to change number of players (up to 8) and number of subsequent colors to win. Have not actually tried it with 8 players but its also really fun single player game when not trying to win. Really hard with 8 different colors and 2 to win :slight_smile:

… and minesweeper has now rainbow colors, as requested by my daughter.

1 Like