module Sidtool

Constants

CLOCK_FREQUENCY
EndOfTrack
FRAMES_PER_SECOND

PAL properties

KeySignature
NoteOff
NoteOn
ProgramChange
STATE
TimeSignature
TrackName
VERSION

Public Class Methods

new(synths_for_voices) click to toggle source
# File lib/sidtool/midi_file_writer.rb, line 93
def initialize(synths_for_voices)
  @synths_for_voices = synths_for_voices
end

Public Instance Methods

build_track(synths) click to toggle source
# File lib/sidtool/midi_file_writer.rb, line 106
def build_track(synths)
  waveforms = [:tri, :saw, :pulse, :noise]

  track = []
  current_frame = 0
  synths.each do |synth|
    channel = waveforms.index(synth.waveform) || raise("Unknown waveform #{synth.waveform}")
    track << DeltaTime[synth.start_frame - current_frame]
    track << NoteOn[channel, synth.tone]
    current_frame = synth.start_frame

    current_tone = synth.tone
    synth.controls.each do |start_frame, tone|
      track << DeltaTime[start_frame - current_frame]
      track << NoteOff[channel, current_tone]
      track << DeltaTime[0]
      track << NoteOn[channel, tone]
      current_tone = tone
      current_frame = start_frame
    end

    end_frame = [current_frame, synth.start_frame + (FRAMES_PER_SECOND * (synth.attack + synth.decay + synth.sustain_length)).to_i].max
    track << DeltaTime[end_frame - current_frame]
    track << NoteOff[channel, current_tone]

    current_frame = end_frame
  end

  track
end
write_to(path) click to toggle source
# File lib/sidtool/midi_file_writer.rb, line 97
def write_to(path)
  tracks = @synths_for_voices.map { |synths| build_track(synths) }

  File.open(path, 'wb') do |file|
    write_header(file)
    tracks.each_with_index { |track, index| write_track(file, track, "Voice #{index + 1}") }
  end
end

Private Instance Methods

write_byte(file, value) click to toggle source
# File lib/sidtool/midi_file_writer.rb, line 193
def write_byte(file, value)
  bytes = [value & 255]
  file << bytes.pack('c')
end
write_header(file) click to toggle source
# File lib/sidtool/midi_file_writer.rb, line 138
def write_header(file)
  # Type
  file << 'MThd'

  # Length
  write_uint32(file, 6)

  # Format
  write_uint16(file, 1)

  # Number of tracks
  write_uint16(file, 3)

  # Division
  # Default tempo is 120 BPM - 120 quarter-notes per minute. Which is 2 quarter-notes per second. If we then define
  # 25 ticks per quarter-note, we end up with a timing of 50 ticks per second.
  write_uint16(file, 25)
end
write_track(file, track, name) click to toggle source
# File lib/sidtool/midi_file_writer.rb, line 157
def write_track(file, track, name)
  track_with_metadata = [
    DeltaTime[0], TrackName[name],
    DeltaTime[0], TimeSignature[4, 2, 24, 8],
    DeltaTime[0], KeySignature[0, 0],

    DeltaTime[0], ProgramChange[0, 1],  # Triangular - maps to piano
    DeltaTime[0], ProgramChange[1, 25], # Saw - maps to guitar
    DeltaTime[0], ProgramChange[2, 33], # Pulse - maps to bass
    DeltaTime[0], ProgramChange[3, 41]  # Noise -  maps to strings
  ] +
  track +
  [
    DeltaTime[0], EndOfTrack[]
  ]
  track_bytes = track_with_metadata.flat_map(&:bytes)

  # Type
  file << 'MTrk'

  # Length
  write_uint32(file, track_bytes.length)

  file << track_bytes.pack('c' * track_bytes.length)
end
write_uint16(file, value) click to toggle source
# File lib/sidtool/midi_file_writer.rb, line 188
def write_uint16(file, value)
  bytes = [(value >> 8) & 255, value & 255]
  file << bytes.pack('cc')
end
write_uint32(file, value) click to toggle source
# File lib/sidtool/midi_file_writer.rb, line 183
def write_uint32(file, value)
  bytes = [(value >> 24) & 255, (value >> 16) & 255, (value >> 8) & 255, value & 255]
  file << bytes.pack('cccc')
end