require 'set'
##
# Including this module can make your class have the ability
# to have an event list.
# See docs below to get an overview of its functions.
module Alda::EventList
##
# The array containing the events (Alda::Event objects),
# most of which are Alda::EventContainer objects.
attr_accessor :events
##
# The set containing the available variable names.
attr_accessor :variables
def on_contained
super if defined? super
instance_eval &@block if @block
end
##
# :call-seq:
# (some sugar) -> Alda::EventContainer
#
# Make the object have the ability to append 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. Ending with 2 underlines: set variable. See Alda::SetVariable.
#
# 2. Starting with 2 lowercase letters and
# ending with underline character: instrument. See Alda::Part.
#
# 3. Starting with 2 lowercase letters: inline lisp code,
# set variable, or get variable.
# One of the above three is chosen intelligently.
# See Alda::InlineLisp, Alda::SetVariable, Alda::GetVariable.
#
# 4. Starting with "t": CRAM. See Alda::Cram.
#
# 5. Starting with one of "a", "b", ..., "g": note. See Alda::Note.
#
# 6. Starting with "r": rest. See Alda::Rest.
#
# 7. "x": chord. See Alda::Chord.
#
# 8. "s": sequence. See Alda::Sequence.
#
# 9. Starting with "o": octave. See Alda::Octave.
#
# 10. Starting with "v": voice. See Alda::Voice.
#
# 11. Starting with "__" (2 underlines): at marker. See Alda::AtMarker.
#
# 12. Starting with "_" (underline) and ending with "_" (underline):
# lisp identifier. See Alda::LispIdentifier.
#
# 13. Starting with "_" (underline): marker. See Alda::Marker.
#
# All the appended events are contained in an Alda::EventContainer object,
# which is to be returned.
#
# These sugars forms a DSL. See ::new for examples.
def method_missing name, *args, &block
if @parent&.respond_to? name, true
return @parent.__send__ name, *args, &block
end
sequence_sugar = ->event do
if args.size == 1
Alda::Sequence.join event, args.first.tap(&:detach_from_parent)
else
event
end
end
case
when /\A(?
[a-z][a-z].*)__\z/ =~ name
Alda::SetVariable.new head, *args, &block
when /\A(?[a-z][a-z].*)_\z/ =~ name
if args.first.is_a? String
Alda::Part.new [part], args.first
else
sequence_sugar.(Alda::Part.new [part])
end
when /\A[a-z][a-z].*\z/ =~ name
arg = args.first
if block || !has_variable?(name) && args.size == 1 && arg.is_a?(Alda::Event) &&
!arg.is_a?(Alda::InlineLisp) && !arg.is_a?(Alda::LispIdentifier) &&
!(arg.is_a?(Alda::EventContainer) &&
(arg.event.is_a?(Alda::InlineLisp) || arg.event.is_a?(Alda::LispIdentifier)))
Alda::SetVariable.new name, *args, &block
elsif has_variable?(name) && (args.empty? || args.size == 1 && arg.is_a?(Alda::Event))
sequence_sugar.(Alda::GetVariable.new name)
else
Alda::InlineLisp.new name, *args
end
when /\At(?.*)\z/ =~ name
Alda::Cram.new duration, &block
when /\A(?[a-g])(?.*)\z/ =~ name
sequence_sugar.(Alda::Note.new pitch, duration)
when /\Ar(?.*)\z/ =~ name
sequence_sugar.(Alda::Rest.new duration)
when /\Ax\z/ =~ name
Alda::Chord.new &block
when /\As\z/ =~ name
Alda::Sequence.new *args, &block
when /\Ao!\z/ =~ name
sequence_sugar.(Alda::Octave.new('').tap { _1.up_or_down = 1})
when /\Ao\?\z/ =~ name
sequence_sugar.(Alda::Octave.new('').tap { _1.up_or_down = -1})
when /\Ao(?\d*)\z/ =~ name
sequence_sugar.(Alda::Octave.new num)
when /\Av(?\d+)\z/ =~ name
sequence_sugar.(Alda::Voice.new num)
when /\A__(?.+)\z/ =~ name
sequence_sugar.(Alda::AtMarker.new head)
when /\A_(?.+)_\z/ =~ name
sequence_sugar.(Alda::LispIdentifier.new head)
when /\A_(?.+)\z/ =~ name
sequence_sugar.(Alda::Marker.new head)
else
super
end.then do |event|
Alda::EventContainer.new event, self
end.tap { @events.push _1 }
end
##
# :call-seq:
# has_variable?(name) -> true or false
#
# Whether there is a previously declared alda variable
# whose name is specified by +name+.
#
# Searches variables in #parent.
def has_variable? name
@variables.include?(name) || !!@parent&.has_variable?(name)
end
##
# :call-seq:
# import(event_list) -> nil
#
# Append the events of another Alda::EventList object here.
# This method covers the disadvantage of alda's being unable to
# import scores from other files.
# See https://github.com/alda-lang/alda-core/issues/8.
def import event_list
@events.concat event_list.events
nil
end
##
# :call-seq:
# new(&block) -> Alda::EventList
#
# +block+ is to be passed with the Alda::EventList object as +self+.
#
# Note that +block+ is not called immediately.
# It is instead called in #on_contained.
# Specially, Alda::Score::new calls #on_contained.
#
# 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 # a sequence
# d4_8 # cannot have '~', use '_' instead
# o3 b8 o4 c2 # a sequence
# end
# # => #
#
# For a list of sugars, see #method_missing.
def initialize &block
@events ||= []
@variables ||= Set.new
@block ||= block
end
##
# :call-seq:
# to_a -> Array
#
# Same as #events.
def to_a
@events
end
##
# :call-seq:
# events_alda_codes(delimiter=" ") -> String
#
# 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
##
# Includes Alda::EventList and provides methods to #play, #parse, or #export.
class Alda::Score
include Alda::EventList
##
# :call-seq:
# play(**opts) -> String
#
# Plays the score.
#
# Returns the command line output of the +alda+ command.
#
# Run command alda help to see available options
# that can be specified in +opts+.
#
# Alda::Score.new { piano_; c; d; e }.play
# # => "[27713] Parsing/evaluating...\n[27713] Playing...\n"
# # (and plays the sound)
# Alda::Score.new { piano_; c; d; e }.play from: 1
# # (plays only an E note)
def play **opts
Alda.play code: self, **opts
end
##
# :call-seq:
# parse(**opts) -> String
#
# Parses the score.
#
# Returns the JSON string of the parse result.
#
# Run command alda help to see available options
# that can be specified in +opts+.
#
# Alda::Score.new { piano_; c }.parse output: :events
# # => "[{\"event-type\":...}]\n"
def parse **opts
Alda.parse code: self, **opts
end
##
# :call-seq:
# export(**opts) -> String
#
# Exports the score.
#
# Returns the command line output of the +alda+ command.
#
# Run command alda help to see available options
# that can be specified in +opts+.
#
# Alda::Score.new { piano_; c }.export output: 'temp.mid'
# # (outputs a midi file called temp.mid)
def export **opts
Alda.export code: self, **opts
end
##
# :call-seq:
# save(filename) -> nil
#
# Saves the alda codes into a file.
def save filename
File.open(filename, 'w') { _1.puts to_s }
end
##
# :call-seq:
# load(filename) -> Alda::InlineLisp
#
# Loads alda codes from a file.
#
# Actually appends a Alda::InlineLisp event of +alda-code+ lisp call.
def load filename
event = Alda::InlineLisp.new :alda_code, File.read(filename)
@events.push event
event
end
##
# :call-seq:
# to_s() -> String
#
# Returns a String containing the alda codes representing the score.
def to_s
events_alda_codes
end
##
# :call-seq:
# new(&block) -> Alda::Score
#
# Creates an Alda::Score.
def initialize(...)
super
on_contained
end
##
# :call-seq:
# clear() -> nil
#
# Clears all the events and variables.
def clear
@events.clear
@variables.clear
nil
end
end