require 'alda-rb/version'
class Array
def to_alda_code
"[#{map(&:to_alda_code).join ' '}]"
end
end
class Hash
def to_alda_code
"{#{to_a.flatten.map(&:to_alda_code).join ' '}}"
end
end
class String
def to_alda_code
inspect
end
end
class Symbol
def to_alda_code
?: + to_s
end
end
class Numeric
def to_alda_code
inspect
end
end
class Proc
# Runs +self+ for +n+ times.
def * n
if !lambda? || arity == 1
n.times &self
else
n.times { self.() }
end
end
end
# The module serving as a namespace.
module Alda
# The path to the +alda+ executable.
#
# The default value is "alda",
# which will depend on your PATH.
singleton_class.attr_accessor :executable
@executable = 'alda'
# The method give Alda# ability to invoke +alda+ at the command line,
# using +name+ as subcommand and +args+ as arguments.
#
# The return value is the string output by the command in STDOUT.
#
# If the exit code is nonzero, a CommandLineError# is raised.
# @example
# Alda.version
# # => "Client version: 1.4.0\nServer version: [27713] 1.4.0\n"
# Alda.parse '-c', 'bassoon: o3 c'
# # => "{\"chord-mode\":false,\"current-instruments\":...}\n"
# Alda.sandwich
# # => Alda::CommandLineError (Expected a command, got sandwich)
def self.method_missing name, *args
name = name.to_s.gsub ?_, ?-
output = IO.popen [executable, name, *args], &:read
raise CommandLineError.new $?, output if $?.exitstatus.nonzero?
output
end
# @return Whether the alda server is up.
def self.up?
status.include? 'up'
end
# @return Whether the alda server is down.
def self.down?
status.include? 'down'
end
# The error raised when one tries to run a non-existing subcommand
# of +alda+.
class CommandLineError < Exception
# The Process::Status object representing the status of
# the process that runs +alda+ command.
attr_accessor :status
# Create a CommandLineError# object.
# @param status The status of the process running +alda+ command.
# @param msg The exception message.
def initialize status, msg = nil
super msg
@status = status
end
end
# Including this module can make your class have the ability
# to have a event list.
# See docs below to get an overview of its functions.
module EventList
# The array containing the events (Event# objects),
# most of which are EventContainer# objects.
attr_accessor :events
# Make the object have the ability to appending its +events+
# conveniently.
#
# Here is a list of sugar. When the name of a method meets certain
# condition, the method is regarded as an event appended to +events+.
#
# 1. Starting with 2 lowercase letters and
# ending with underline character: instrument. See Part#.
#
# 2. Starting with 2 lowercase letters: inline lisp code.
# See InlineLisp#.
#
# 3. Starting with "t": CRAM. See Cram#.
#
# 4. Starting with one of "a", "b", ..., "g": note. See Note#.
#
# 5. Starting with "r": rest. See Rest#.
#
# 6. Starting with "x": chord. See Chord#.
#
# 7. Starting with "o": octave. See Octave#.
#
# 8. Starting with "v": voice. See Voice#.
#
# 9. Starting with "__" (2 underlines): at marker. See AtMarker#.
#
# 10. Starting with "_" (underline): marker. See Marker#.
#
# Notes cannot have dots.
# To tie multiple durations, +_+ is used instead of +~+.
#
# All the appended events are contained in a EventContainer# object,
# which is to be returned.
#
# These sugars forms a DSL.
# @see #initialize.
# @return an EventContainer# object.
def method_missing name, *args, &block
case
when /^(?[a-z][a-z].*)_$/ =~ name
Part.new [part], args.first
when /^[a-z][a-z].*$/ =~ name
InlineLisp.new name, *args
when /^t(?.*)$/ =~ name
Cram.new duration, &block
when /^(?[a-g])(?.*)$/ =~ name
Note.new pitch, duration
when /^r(?.*)$/ =~ name
Rest.new duration
when /^x$/ =~ name
Chord.new &block
when /^o(?\d*)$/ =~ name
Octave.new num
when /^v(?\d+)$/ =~ name
Voice.new num
when /^__(?.+)$/ =~ name
AtMarker.new head
when /^_(?.+)$/ =~ name
Marker.new head
else
super
end.then do |event|
EventContainer.new event, self
end.tap &@events.method(:push)
end
# Append the events of another EventList# object here.
# This method covers the disadvantage of alda's being unable to
# import scores from other files.
def import event_list
@events += event_list.events
end
# @param block to be passed with the EventList# object as +self+.
# @example
# Alda::Score.new do
# tempo! 108 # inline lisp
# piano_ # piano part
# o4 # octave 4
# c8; d; e; f # notes
# g4; g; a; f; g; e; f; d; e; c
# d4_8 # cannot have '~', use '_' instead
# o3 b8 o4 c2
# end
# # => #
def initialize &block
@events ||= []
instance_eval &block if block
end
# Same as #events
def to_a
@events
end
# Join the alda codes of #events with a specified delimiter.
# Returns a string representing the result.
def events_alda_codes delimiter = ' '
@events.map(&:to_alda_code).join delimiter
end
end
# The class mixes in EventList# and provides a method to play.
class Score
include EventList
# Plays the score.
# @return the command line output of the +alda+ command.
# @example
# Alda::Score.new { piano_; c; d; e }.play
# # => "[27713] Parsing/evaluating...\n[27713] Playing...\n"
# # (and plays the sound)
def play
Alda.stop
Alda.play '--code', events_alda_codes
end
end
# The class of elements of EventList#events.
class Event
# The EventList# object that contains it.
# Note that it may not be directly contained, but with a EventContainer#
# object in the middle.
attr_accessor :parent
# The callback invoked when it is contained in a EventContainer#.
# It is overridden in InlineLisp#, so be aware if you want to
# override InlineLisp#on_contained.
# @example
# class Alda::Note
# def on_contained
# puts 'a note contained'
# end
# end
# Alda::Score.new { c } # => outputs "a note contained"
def on_contained
end
# Converts to alda code. To be overridden.
def to_alda_code
''
end
end
# The class for objects containing an event.
class EventContainer < Event
# The contained Event# object.
attr_accessor :event
# @param event The Event# object to be contained.
# @param parent The EventList# object containing the event.
def initialize event, parent
@event = event
@parent = parent
@event.parent = @parent
@event.on_contained
end
# Make #event a Chord# object.
# @example
# Alda::Score.new { piano_; c/-e/g }.play
# # (plays the chord Cm)
#
# If the contained event is a Part# object,
# make #event a new Part# object.
# @example
# Alda::Score.new { violin_/viola_/cello_; e; f; g}.play
# # (plays notes E, F, G with three instruments simultaneously)
def / other
raise unless other == @parent.events.pop
@event =
if @event.is_a? Part
Part.new @event.names + other.event.names, other.event.arg
else
Chord.new @event, other.event
end
self
end
def to_alda_code
@event.to_alda_code
end
def method_missing name, *args
@event.__send__ name, *args
end
end
# Inline lisp event.
class InlineLisp < Event
# The function name of the lisp function
attr_accessor :head
# The arguments passed to the lisp function.
# Its elements can be
# Array#, Hash#, Numeric#, String#, Symbol#, or InlineLisp#.
attr_accessor :args
# The underlines in +head+ will be converted to hyphens.
def initialize head, *args
@head = head.to_s.gsub ?_, ?-
@args = args
end
def to_alda_code
"(#{head} #{args.map(&:to_alda_code).join ' '})"
end
def on_contained
@args.reverse_each do |event|
raise if event.is_a?(Event) && event != @parent.events.pop
end
end
end
# A note event.
class Note < Event
# The string representing the pitch
attr_accessor :pitch
# The string representing the duration.
# It ends with +~+ if the note slurs.
attr_accessor :duration
# The underlines in +duration+ will be converted to +~+.
def initialize pitch, duration
@pitch = pitch.to_s
@duration = duration.to_s.gsub ?_, ?~
end
# Append a sharp sign after #pitch.
# @example
# Alda::Score.new { piano_; +c }.play
# # (plays a C\# note)
def +@
@pitch += ?+
self
end
# Append a flat sign after #pitch.
# @example
# Alda::Score.new { piano_; -d }.play
# # (plays a Db note)
def -@
@pitch += ?-
self
end
# Append a natural sign after #pitch
# @example
# Alda::Score.new { piano_; key_sig 'f+'; ~f }.play
# # (plays a F note)
def ~
@pitch += ?_
self
end
def to_alda_code
@pitch + @duration
end
end
# A rest event.
class Rest < Event
# The string representing a duration.
attr_accessor :duration
# Underlines in +duration+ will be converted to +~+.
def initialize duration
@duration = duration.to_s.gsub ?_, ?~
end
def to_alda_code
?r + @duration
end
end
# An octave event.
class Octave < Event
# The string representing the octave's number.
# It can be empty, serving for #+@ and #-@.
attr_accessor :num
# Positive for up, negative for down, and 0 as default.
attr_accessor :up_or_down
def initialize num
@num = num.to_s
@up_or_down = 0
end
# Octave up.
# @example
# Alda::Score.new { piano_; c; +o; c }.play
# # (plays C4, then C5)
# @see #-@
def +@
@up_or_down += 1
self
end
# Octave down.
# @see #+@.
def -@
@up_or_down -= 1
self
end
def to_alda_code
case @up_or_down <=> 0
when 0
?o + @num
when 1
?> * @up_or_down
when -1
?< * -@up_or_down
end
end
end
# A chord event.
# Includes EventList#.
class Chord < Event
include EventList
# EventList#x invokes this method.
# @see EventList#method_missing
# @param events In most cases, should not be used.
# @param block To be passed with the Chord# object as +self+.
# @example
# Alda::Score.new { piano_; x { c; -e; g } }.play
# # (plays chord Cm)
def initialize *events, &block
@events = events
super &block
end
def to_alda_code
events_alda_codes ?/
end
end
# A part event.
class Part < Event
# The names of the part. To be joined with +/+ as delimiter.
attr_accessor :names
# The nickname of the part. +nil+ if none.
attr_accessor :arg
def initialize names, arg = nil
@names = names.map { |name| name.to_s.gsub ?_, ?- }
@arg = arg
end
def to_alda_code
result = @names.join ?/
result += " \"#{@arg}\"" if @arg
result + ?:
end
end
# A voice event.
class Voice < Event
# The string representing the voice's number.
attr_accessor :num
def initialize num
@num = num
end
def to_alda_code
?V + num + ?:
end
end
# A CRAM event. Includes EventList#.
class Cram < Event
include EventList
# The string representing the duration of the CRAM.
attr_accessor :duration
# EventList#t invokes this method.
# @see EventList#method_missing
# @param block To be passed with the CRAM as +self+.
# @example
# Alda::Score.new { piano_; t8 { x; y; }}
def initialize duration, &block
@duration = duration
super &block
end
def to_alda_code
"{#{events_alda_codes}}#@duration"
end
end
# A marker event.
# @see AtMarker#
class Marker < Event
# The marker's name
attr_accessor :name
# Underlines in +name+ is converted to hyphens.
def initialize name
@name = name.to_s.gsub ?_, ?-
end
def to_alda_code
?% + @name
end
end
# An at-marker event
# @see Marker#
class AtMarker < Event
# The corresponding marker's name
attr_accessor :name
# Underlines in +name+ is converted to hyphens.
def initialize name
@name = name.to_s.gsub ?_, ?-
end
def to_alda_code
?@ + @name
end
end
end