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
##
# When the module is included by a subclass of Alda::Event,
# this method overrides Alda::Event#on_contained.
# When invoked, calls the overridden method (if any) and then evaluates the block
# given when ::new was called.
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.
# This will trigger a warning if we are using \Alda 2 because
# parts inside a sequence are not allowed in \Alda 2
# ({alda-lang/alda#441}[https://github.com/alda-lang/alda/discussions/441#discussioncomment-3825064]).
#
# 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
Alda::Utils.warn 'parts in sequence not allowed in v2' if Alda.v2? && !args.empty?
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_event_of?(Alda::InlineLisp) && !arg.is_event_of?(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
#
# The parameter +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
##
# :call-seq:
# event_list == other -> true or false
#
# Returns true if +other+ is of the same class as +event_list+
# and they have the same (in the sense of ==) #events and #variables.
def == other
super || self.class == other.class && @events == other.events && @variables == other.variables
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::Raw
#
# Loads alda codes from a file.
#
# Actually appends a Alda::Raw event with the contents in the file +filename+.
def load filename
event = Alda::Raw.new 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
##
# :call-seq:
# raw(contents) -> Alda::Raw
#
# Adds an Alda::Raw event to the event list and returns it.
# The event is not contained by a container.
#
# Alda::Score.new { raw 'piano: c d e' }.to_s # => "piano: c d e"
def raw contents
Alda::Raw.new(contents).tap { @events.push _1 }
end
##
# :call-seq:
# l(head, *args) -> Alda::EventContainer
#
# Adds an Alda::EventContainer containing an Alda::InlineLisp event to the event list.
# In most cases, #method_misssing is a more convenient way to add an inline Lisp event.
# However, sometimes you may want to programmatically control which Lisp function to be called,
# or the function name is already a valid Ruby method name
# (for example, you want to use +f+ or +p+ as the dynamics but +f+ would be interpreted as a note
# and +p+ is already a Ruby method for printing)
# so that it cannot trigger #method_missing,
# then you should use this method.
#
# Alda::Score.new { piano_; l :p; c }.to_s # => "piano: (p ) c"
def l head, *args
Alda::EventContainer.new(Alda::InlineLisp.new(head, *args), self).tap { @events.push _1 }
end
end