Class: Wavify::DSL::SongDefinition

Inherits:
Object
  • Object
show all
Defined in:
lib/wavify/dsl.rb,
sig/dsl.rbs

Overview

Immutable compiled song definition returned by build_definition.

Defined Under Namespace

Classes: Section

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(format:, tempo:, beats_per_bar:, swing:, default_bars:, tracks:, sections:, random_seed:) ⇒ SongDefinition

Returns a new instance of SongDefinition.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/wavify/dsl.rb', line 19

def initialize(format:, tempo:, beats_per_bar:, swing:, default_bars:, tracks:, sections:, random_seed:)
  @format = format
  @tempo = tempo
  @beats_per_bar = beats_per_bar
  @swing = swing
  @default_bars = default_bars
  @random_seed = random_seed
  @tracks = tracks.map(&:finalize!).freeze
  @sections = sections.map do |section|
    section.tracks = section.tracks.dup.freeze
    section.markers = section.markers.dup.freeze
    section.freeze
  end.freeze
  freeze
end

Instance Attribute Details

#beats_per_barInteger (readonly)

Returns the value of attribute beats_per_bar.

Returns:

  • (Integer)


17
18
19
# File 'lib/wavify/dsl.rb', line 17

def beats_per_bar
  @beats_per_bar
end

#default_barsInteger (readonly)

Returns the value of attribute default_bars.

Returns:

  • (Integer)


17
18
19
# File 'lib/wavify/dsl.rb', line 17

def default_bars
  @default_bars
end

#formatCore::Format (readonly)

Returns the value of attribute format.

Returns:



17
18
19
# File 'lib/wavify/dsl.rb', line 17

def format
  @format
end

#random_seedInteger (readonly)

Returns the value of attribute random_seed.

Returns:

  • (Integer)


17
18
19
# File 'lib/wavify/dsl.rb', line 17

def random_seed
  @random_seed
end

#sectionsArray[untyped] (readonly)

Returns the value of attribute sections.

Returns:

  • (Array[untyped])


17
18
19
# File 'lib/wavify/dsl.rb', line 17

def sections
  @sections
end

#swingFloat (readonly)

Returns the value of attribute swing.

Returns:

  • (Float)


17
18
19
# File 'lib/wavify/dsl.rb', line 17

def swing
  @swing
end

#tempoFloat (readonly)

Returns the value of attribute tempo.

Returns:

  • (Float)


17
18
19
# File 'lib/wavify/dsl.rb', line 17

def tempo
  @tempo
end

#tracksArray[untyped] (readonly)

Returns the value of attribute tracks.

Returns:

  • (Array[untyped])


17
18
19
# File 'lib/wavify/dsl.rb', line 17

def tracks
  @tracks
end

Instance Method Details

#arrangementArray<Hash>

Returns arrangement sections as hashes accepted by the sequencer engine.

Returns:

  • (Array<Hash>)


84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/wavify/dsl.rb', line 84

def arrangement
  @sections.map do |section|
    {
      name: section.name,
      bars: section.bars,
      tracks: section.tracks,
      repeat: section.repeat || 1,
      tempo: section.tempo,
      beats_per_bar: section.beats_per_bar,
      markers: section.markers
    }.freeze
  end
end

#arrangement?Boolean

Returns:

  • (Boolean)


35
36
37
# File 'lib/wavify/dsl.rb', line 35

def arrangement?
  !@sections.empty?
end

#duration(default_bars: @default_bars) ⇒ Wavify::Core::Duration Also known as: length

Planned song duration derived from arrangement/default bars.

Parameters:

  • default_bars (Integer) (defaults to: @default_bars)
  • default_bars: (Integer) (defaults to: @default_bars)

Returns:



102
103
104
# File 'lib/wavify/dsl.rb', line 102

def duration(default_bars: @default_bars)
  Core::Duration.new(total_seconds(default_bars: default_bars))
end

#engineWavify::Sequencer::Engine

Returns a sequencer engine configured from the song definition.



42
43
44
45
46
47
48
49
# File 'lib/wavify/dsl.rb', line 42

def engine
  Wavify::Sequencer::Engine.new(
    tempo: @tempo,
    format: @format,
    beats_per_bar: @beats_per_bar,
    swing: @swing
  )
end

#render(default_bars: @default_bars, stems: false) ⇒ Wavify::Audio

Renders the song definition to an Audio instance.

Parameters:

  • default_bars (Integer) (defaults to: @default_bars)
  • stems (Boolean) (defaults to: false)

    return track-name keyed stems instead of a mix

  • default_bars: (Integer) (defaults to: @default_bars)
  • stems: (Boolean) (defaults to: false)

Returns:



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/wavify/dsl.rb', line 143

def render(default_bars: @default_bars, stems: false)
  return render_stems(default_bars: default_bars) if stems

  playable_tracks = sequencer_tracks.select { |track| sequencer_track_playable?(track) }
  sequencer_audio = if playable_tracks.empty?
                      Wavify::Audio.silence(0, format: @format)
                    else
                      playable_names = playable_tracks.map(&:name)
                      playable_arrangement = if arrangement?
                                               arrangement.map do |section|
                                                 section.merge(tracks: section.fetch(:tracks) & playable_names)
                                               end
                                             end
                      engine.render(
                        tracks: playable_tracks,
                        arrangement: playable_arrangement,
                        default_bars: default_bars
                      )
                    end
  sample_audio = render_sample_tracks(default_bars: default_bars)

  rendered = if sample_audio.nil?
               sequencer_audio
             elsif sequencer_audio.sample_frame_count.zero?
               sample_audio
             else
               Wavify::Audio.mix(sequencer_audio, sample_audio, strategy: :none)
             end
  pad_to_song_duration(rendered, default_bars: default_bars)
end

#sequencer_tracksArray<Wavify::Sequencer::Track>

Returns sequencer tracks converted from DSL track definitions.

Returns:



54
55
56
# File 'lib/wavify/dsl.rb', line 54

def sequencer_tracks
  @tracks.map { |track| with_track_context(track) { track.to_sequencer_track } }
end

#timeline(default_bars: @default_bars) ⇒ Array<Hash>

Builds a sequencer event timeline without rendering audio.

Parameters:

  • default_bars (Integer) (defaults to: @default_bars)
  • default_bars: (Integer) (defaults to: @default_bars)

Returns:

  • (Array<Hash>)


112
113
114
115
116
117
118
# File 'lib/wavify/dsl.rb', line 112

def timeline(default_bars: @default_bars)
  engine.build_timeline(
    tracks: sequencer_tracks,
    arrangement: arrangement? ? arrangement : nil,
    default_bars: default_bars
  )
end

#timeline_json(default_bars: @default_bars) ⇒ String

Serializes the sequencer timeline to JSON for visualization tooling.

Parameters:

  • default_bars (Integer) (defaults to: @default_bars)
  • default_bars: (Integer) (defaults to: @default_bars)

Returns:

  • (String)


124
125
126
# File 'lib/wavify/dsl.rb', line 124

def timeline_json(default_bars: @default_bars)
  JSON.generate(timeline(default_bars: default_bars))
end

#timeline_text(default_bars: @default_bars) ⇒ String

Renders the sequencer timeline as a compact text table.

Parameters:

  • default_bars (Integer) (defaults to: @default_bars)
  • default_bars: (Integer) (defaults to: @default_bars)

Returns:

  • (String)


132
133
134
135
136
# File 'lib/wavify/dsl.rb', line 132

def timeline_text(default_bars: @default_bars)
  events = timeline(default_bars: default_bars)
  rows = events.map { |event| timeline_text_row(event) }
  (["time\tbar\ttrack\tkind\tdetail"] + rows).join("\n")
end

#validate!(deep: false) ⇒ true

Validates the compiled song without rendering audio.

Parameters:

  • deep: (Boolean) (defaults to: false)

Returns:

  • (true)

Raises:



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/wavify/dsl.rb', line 61

def validate!(deep: false)
  raise Wavify::SequencerError, "song must define at least one track" if @tracks.empty?

  @tracks.each do |track|
    with_track_context(track) do
      track.to_sequencer_track
      track.sample_pattern_map
      if deep
        track.samples.each_value do |path|
          Wavify::Audio.(path)
        rescue Wavify::Error, SystemCallError => e
          raise Wavify::SequencerError, "failed to inspect sample #{path.inspect}: #{e.message}"
        end
      end
    end
  end
  timeline(default_bars: @default_bars)
  true
end

#write(path, default_bars: @default_bars) ⇒ Wavify::Audio

Renders and writes the song to disk.

Parameters:

  • path (String)
  • default_bars (Integer) (defaults to: @default_bars)
  • default_bars: (Integer) (defaults to: @default_bars)

Returns:



179
180
181
# File 'lib/wavify/dsl.rb', line 179

def write(path, default_bars: @default_bars)
  render(default_bars: default_bars).write(path)
end

#write_stems(directory, default_bars: @default_bars, overwrite: true) ⇒ Hash<Symbol, String>

Renders each track and writes stems as WAV files under a directory.

Parameters:

  • directory (String)
  • default_bars (Integer) (defaults to: @default_bars)
  • overwrite (Boolean) (defaults to: true)
  • default_bars: (Integer) (defaults to: @default_bars)
  • overwrite: (Boolean) (defaults to: true)

Returns:

  • (Hash<Symbol, String>)

Raises:



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/wavify/dsl.rb', line 189

def write_stems(directory, default_bars: @default_bars, overwrite: true)
  raise Wavify::InvalidParameterError, "directory must be a String" unless directory.is_a?(String)

  expanded_directory = File.expand_path(directory)
  FileUtils.mkdir_p(File.dirname(expanded_directory))
  stems = render(default_bars: default_bars, stems: true)
  filenames = stems.keys.to_h { |track_name| [track_name, stem_filename(track_name)] }
  if filenames.values.uniq.length != filenames.length
    raise Wavify::SequencerError, "track names produce duplicate stem filenames"
  end
  paths = filenames.transform_values { |filename| File.join(expanded_directory, filename) }
  if !overwrite && (existing = paths.values.find { |path| File.exist?(path) })
    raise Wavify::InvalidParameterError, "output file already exists: #{existing}"
  end

  Dir.mktmpdir(".wavify-stems-", File.dirname(expanded_directory)) do |temporary_directory|
    stems.each do |track_name, audio|
      audio.write(File.join(temporary_directory, filenames.fetch(track_name)))
    end
    FileUtils.mkdir_p(expanded_directory)
    paths.each do |track_name, path|
      File.rename(File.join(temporary_directory, filenames.fetch(track_name)), path)
    end
  end
  paths
end