Created
November 3, 2020 22:30
-
-
Save miura1729/1713c25cde3212c8aa7a0f1b1527b6b6 to your computer and use it in GitHub Desktop.
SMAF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# SMAF operation class | |
# Copyright (c) 2002 Hideki MIURA | |
# You can redistribute it and/or modify it under the same term as Ruby. | |
# | |
# Copyright of SMAF belonges to YAMAHA Corpration | |
# | |
module SMAF | |
# Block base class | |
# This class is common operationes for all block. Block is word in | |
# this program. Block is chunk, event, sub-block. | |
# | |
# | |
class Block | |
@@default_hook = Hash.new(nil) | |
@@default_hook[Block] = { | |
:before_parse => lambda {|c| }, | |
:after_parse => lambda {|c| }, | |
} | |
def self.get_default_hook | |
@@default_hook[self] | |
end | |
def self.set_default_hook(key, proc) | |
@@default_hook[self][key] = proc | |
end | |
def self.inherited(sub) | |
@@default_hook[sub] = @@default_hook[Block].dup | |
end | |
def initialize(id, parent) | |
@parent = parent | |
@id = id | |
@data = nil | |
@childlen = [] | |
@hook = self.class.get_default_hook.dup | |
end | |
def init_from_stream(inport) | |
parse_body(inport) | |
end | |
def parse_body(inport) | |
@hook[:before_parse].call(self) | |
n = parse_body_no_hook(inport) | |
@hook[:after_parse].call(self) | |
n | |
end | |
def to_s | |
"#<Block: #{@id} Size=#{@size}>" | |
end | |
attr :parent | |
def get_childlen | |
@childlen | |
end | |
def get_id | |
@id | |
end | |
def get_size | |
@size | |
end | |
private | |
def id2class | |
nil | |
end | |
def parse_body_no_hook(inport) | |
@data = inport.read(@size) | |
@size | |
end | |
end | |
# Chunk base class | |
# This class is common operationes for all chunk. | |
# | |
# Chunk consists of chunkID, chunkSize, body and crc | |
# | |
class Chunk<Block | |
def init_from_stream(inport) | |
@size = (inport.read(4).unpack('N'))[0] | |
@restsize = @size | |
super | |
@size + 4 | |
end | |
def to_s | |
"#<Chunk: #{@id} Size=#{@size}>" | |
end | |
private | |
def parse_childlen(inport, rest) | |
chunks = [] | |
while rest > 3 do | |
id = inport.read(4) | |
rest = rest - 4 | |
cls = id2class(id) | |
if cls == nil then | |
cls = id2class(id[0,3]) | |
end | |
if cls == nil then | |
raise "Unknown Chunk id #{id}" | |
end | |
nc = cls.new(id, self) | |
rest -= nc.init_from_stream(inport) | |
chunks.push nc | |
end | |
chunks | |
end | |
end | |
# | |
# Event Holder Chunk Abstruct Class | |
# | |
# | |
class EventHolderChunk<Chunk | |
include EventUtil | |
def initialize(chunk_id, parent) | |
@timebase = parent.get_timebase_as_msec | |
super | |
end | |
def init_from_stream(inport) | |
super | |
end | |
def get_timebase_as_msec | |
@timebase | |
end | |
private | |
def id2class(key) | |
raise "Event Holder Chunk class is abstruct." | |
end | |
def parse_body_no_hook(inport) | |
# Seqence size of 0 data in input stream | |
seq0size = 0 | |
curtime = 0 | |
orgrest = @restsize | |
begin | |
dur = parse_duration(inport) | |
@restsize -= duration_size(dur) | |
if dur != 0 then | |
seq0size = 0 | |
else | |
seq0size = seq0size + 1 | |
end | |
curtime = curtime + dur * @timebase | |
etype = inport.readchar | |
@restsize -= 1 | |
if etype != 0 then | |
seq0size = 0 | |
else | |
seq0size = seq0size + 1 | |
end | |
ev = make_event(etype, inport, curtime) | |
if ev then | |
@childlen.push(ev) | |
seq0size = 0 | |
end | |
if @restsize < 2 then | |
inport.read(@restsize) | |
@restsize = 0 | |
end | |
end while @restsize > 1 | |
orgrest | |
end | |
end | |
class ContentsInfoChunk<Chunk; end | |
class OptionalDataChunk<Chunk; end | |
class MasterTrackChunk<Chunk; end | |
class SequenceDataChunk<EventHolderChunk | |
def initialize(chunk_id, parent) | |
@timebase = parent.get_timebase_as_msec | |
super | |
end | |
private | |
def make_event(etype, inport, curtime) | |
ev = nil | |
if etype == 255 then | |
nx = inport.readchar | |
@restsize -= 1 | |
if nx == 0xf0 then | |
size = inport.readchar | |
inport.read(size) | |
@restsize -= (size + 1) | |
ev = nil | |
else | |
ev = NopMessage.new(0, 0, self, curtime) | |
end | |
elsif etype == 0 then | |
etype = inport.readchar | |
@restsize -= 1 | |
if etype == 0 then | |
return nil | |
end | |
ch = etype >> 6 | |
case etype & 0x30 | |
when 0x30 | |
val = inport.readchar | |
@restsize -= 1 | |
cls = {0x0 => ProgramChangeMessage, | |
0x1 => BankSelectMessage, | |
0x2 => OctaveShiftMessage, | |
0x3 => ModulationMessage, | |
0x4 => PitchBendMessage, | |
0x7 => VolumeMessage, | |
0xa => PanMessage, | |
0xb => ExpressionMessage}[etype & 0xf] | |
ev = cls.new(ch, val, self, curtime) | |
when 0x20 | |
val = ((etype & 0xf) + 1) * 0x8 | |
ev = ModulationMessage.new(ch, val, self, curtime) | |
when 0x10 | |
val = ((etype & 0xf) + 1) * 0x8 | |
ev = PitchBendMessage.new(ch, val, self, curtime) | |
when 0x0 | |
val = ((etype & 0xf) + 1) * 0x8 | |
ev = ExpressionMessage.new(ch, val, self, curtime) | |
end | |
else | |
gt = parse_duration(inport) | |
@restsize -= duration_size(gt) | |
ev = NoteMessage.new(etype, gt, self, curtime) | |
end | |
ev | |
end | |
end | |
class ScoreTrackChunk<Chunk | |
include TimeBaseUtil | |
ID2Class = { | |
'MspI' => Chunk, | |
'Mtsu' => Chunk, | |
'Mtsq' => SequenceDataChunk | |
} | |
def id2class(key) | |
ID2Class[key] | |
end | |
def get_timebase_as_msec | |
@timebase_d | |
end | |
def get_gate_timebase_as_msec | |
@timebase_g | |
end | |
def parse_body_no_hook(inport) | |
@format_type = inport.readchar | |
@sequence_type = inport.readchar | |
case @format_type | |
when 0 | |
@timebase_d = timebase2msec(inport.readchar) | |
@timebase_g = timebase2msec(inport.readchar) | |
data = [] | |
data[0] = inport.readchar | |
data[1] = inport.readchar | |
@key_control_status = [] | |
@vibration_status = [] | |
@ch_type = [] | |
(0..1).each do |n| | |
n2 = n * 2 | |
@key_control_status[n2] = (data[n] >> 7) & 1 | |
@key_control_status[n2 + 1] = (data[n] >> 3) & 1 | |
@vibration_status[n2] = (data[n] >> 6) & 1 | |
@vibration_status[n2 + 1] = (data[n] >> 2) & 1 | |
@ch_type[n2] = (data[n] >> 4) & 3 | |
@ch_type[n2 + 1] = (data[n] >> 0) & 3 | |
end | |
@childlen = parse_childlen(inport, @size - 6) | |
end | |
@size | |
end | |
end | |
class AudioTrackChunk<Chunk; end | |
# | |
# Graphics Track Sequence Data Chunk | |
# See Sepc. 70 | |
# | |
class GraphicsTrackSequenceDataChunk<EventHolderChunk | |
def initialize(chunk_id, parent) | |
@timebase = parent.get_timebase_as_msec | |
super | |
end | |
private | |
def make_event(etype, inport, curtime) | |
ev = id2class(etype).new(etype, self, curtime) | |
@restsize -= ev.init_from_stream(inport) | |
ev | |
end | |
def id2class(key) | |
case key | |
when 0 | |
NopEvent | |
when 1 | |
ResetOriginEvent | |
when 0x20 | |
BackDropColorDefnitionEvent | |
when 0x21 | |
OffsetOriginEvent | |
when 0x22 | |
ControlEvent # User Event | |
when 0x40..0x7f | |
DisplayObjectEvent | |
else | |
raise "Reserved Evnet Type #{key}" | |
end | |
end | |
end | |
# | |
# Image Chunk | |
# See Spec. 109 | |
# | |
class ImageChunk<Chunk | |
include ImageUtil | |
private | |
def parse_body_no_hook(inport) | |
@image = inport.read(@size) | |
@size | |
end | |
end | |
# | |
# Bmp Chunk | |
# See Spec. 110 | |
# | |
class BmpChunk<Chunk | |
include ImageUtil | |
private | |
def parse_body_no_hook(inport) | |
@xwidth = inport.readchar | |
midbyte = inport.readchar | |
@ywidth = inport.readchar | |
rsize = @size - 3 | |
@xwidth = @xwidth << 4 + ((midbyte >> 4) & 0xf) | |
@ywidth = ((midbyte & 0xf) << 8) + @ywidth | |
@image = inport.read(rsize) | |
@size | |
end | |
end | |
# | |
# Link Chunk | |
# See Spec. 111 | |
# | |
class LinkChunk<Chunk | |
end | |
# | |
# Image Data Chunk | |
# See spec. 109 | |
# | |
class ImageDataChunk<Chunk | |
private | |
ID2Class = { | |
'Gig' => ImageChunk, | |
'Gmd' => BmpChunk, | |
'Gln' => LinkChunk | |
} | |
def id2class(key) | |
ID2Class[key] | |
end | |
def parse_body_no_hook(inport) | |
@childlen = parse_childlen(inport, @size) | |
@size | |
end | |
end | |
# | |
# Display Parameter Definition Chunk | |
# See spec. 68p | |
# | |
class DisplayParameterDefinitionChunk<Chunk | |
include EventUtil | |
include DisplayParameterUtil | |
attr :data | |
private | |
def parse_body_no_hook(inport) | |
@data = {} | |
rest = @size | |
while rest > 0 do | |
esize = parse_duration(inport) | |
etype = inport.readchar | |
@data[etype] = parse_repeat(inport, esize - 1) | |
rest = rest - esize - duration_size(esize) | |
end | |
@size | |
end | |
end | |
# | |
# Graphics Track Setup Data Chunk | |
# See spec. 68p | |
# | |
class GraphicsTrackSetupDataChunk<Chunk | |
private | |
ID2Class = { | |
'Gdpd' => DisplayParameterDefinitionChunk, | |
'Gcpd' => Chunk | |
} | |
def id2class(key) | |
ID2Class[key] | |
end | |
def parse_body_no_hook(inport) | |
@childlen = parse_childlen(inport, @size) | |
@size | |
end | |
end | |
# | |
# Graphics Track Chunk | |
# See spec. 65p | |
# | |
class GraphicsTrackChunk<Chunk | |
include TimeBaseUtil | |
DataOffset = {"Format Type" => 0, "Player Type" => 1, | |
"Text Encode Type" => 2, "Color Type" => 3, | |
"Time Base" => 4, "Option Size" => 5, "Option Data" => 6} | |
def initialize(chunk_id, parent) | |
@time_base = 1 | |
super | |
end | |
def get_data_by_label(label) | |
@data[DataOffset[label]] | |
end | |
def get_timebase_as_msec | |
timebase2msec(@data[4]) | |
end | |
private | |
ID2Class = { | |
'Gtsu' => GraphicsTrackSetupDataChunk, | |
'Gsq' => GraphicsTrackSequenceDataChunk, | |
'Gfid' => Chunk, | |
'Gimd' => ImageDataChunk, | |
} | |
def id2class(key) | |
ID2Class[key] | |
end | |
def parse_body_no_hook(inport) | |
rest = @size | |
@data = inport.read(6) | |
optsize = @data[5].to_i | |
@data = @data + inport.read(optsize) | |
rest = rest - optsize - 6 | |
@time_base = get_timebase_as_msec | |
@childlen = parse_childlen(inport, rest) | |
@size | |
end | |
def to_s | |
"#<Chunk: #{@id} Size=#{@size} ColorType=#{get_data_by_label('Color Type')}>" | |
end | |
end | |
# File chunk | |
# | |
# FileChunk is toplevel chunk in SMAF file | |
# See SMAF spec 16p (in Japanese) | |
# | |
# FileChunk consists of Header (define in chunk.rb), body, CRC | |
# | |
class FileChunk<Chunk | |
# | |
# ChunkID -> ChunkClass | |
# | |
ID2Class = { | |
'CNTI' => ContentsInfoChunk, | |
'OPDA' => OptionalDataChunk, | |
'MSTR' => MasterTrackChunk, | |
'MTR' => ScoreTrackChunk, | |
'ATR' => AudioTrackChunk, | |
'GTR' => GraphicsTrackChunk, | |
} | |
private | |
def id2class(key) | |
ID2Class[key] | |
end | |
def parse_body_no_hook(inport) | |
@childlen = parse_childlen(inport, @size) | |
@crc = inport.read(2) | |
@size + 2 | |
end | |
end | |
# | |
# Chunk generater from stream | |
# | |
# | |
class Parser | |
def parse(inport) | |
inport.binmode | |
id = inport.read(4) | |
top = FileChunk.new(id, nil) | |
top.init_from_stream(inport) | |
top | |
end | |
end | |
end # module SMAF |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment