Skip to content

Commit 06cd187

Browse files
Support for decoding IT/OpenMPT blocks
1 parent b578177 commit 06cd187

7 files changed

Lines changed: 332 additions & 10 deletions

File tree

music/tracked/it/block/block.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package block
2+
3+
import (
4+
"encoding/binary"
5+
)
6+
7+
// Block is a block interface
8+
type Block interface {
9+
FourCC() uint32
10+
Length() int // total length, including the magic identifier
11+
}
12+
13+
type blockBase struct {
14+
Identifier [4]byte
15+
BlockLen uint32
16+
}
17+
18+
// FourCC returns the big-endian representation of the block identifier
19+
func (b *blockBase) FourCC() uint32 {
20+
return binary.BigEndian.Uint32(b.Identifier[:])
21+
}
22+
23+
// Length returns the size of the whole block
24+
func (b *blockBase) Length() int {
25+
return 8 + int(b.BlockLen)
26+
}
27+
28+
type Unknown struct {
29+
blockBase
30+
}
31+
32+
// FourCC returns the big-endian representation of the block identifier
33+
func (b *Unknown) FourCC() uint32 {
34+
return b.blockBase.FourCC()
35+
}
36+
37+
// Length returns the size of the whole block
38+
func (b *Unknown) Length() int {
39+
return b.blockBase.Length()
40+
}

music/tracked/it/block/fx.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package block
2+
3+
import "github.com/gotracker/goaudiofile/internal/util"
4+
5+
type FXPluginType [4]byte
6+
7+
var (
8+
PluginTypeVST = FXPluginType{'P', 's', 't', 'V'}
9+
PluginTypeDMO = FXPluginType{'O', 'M', 'X', 'D'}
10+
)
11+
12+
type FXGainFactor uint8
13+
14+
// Value returns the value as a multiplier
15+
func (g FXGainFactor) Value() float64 {
16+
return float64(g) / 10
17+
}
18+
19+
type FXUserPluginName [32]byte
20+
21+
func (p FXUserPluginName) String() string {
22+
return util.GetString(p[:])
23+
}
24+
25+
type FXLibraryName [64]byte
26+
27+
func (l FXLibraryName) String() string {
28+
return util.GetString(l[:])
29+
}
30+
31+
// FX is a FX__ block
32+
type FX struct {
33+
blockBase
34+
PluginType FXPluginType // Plugin type ("PtsV" for VST, "OMXD" for DMO plugins)
35+
UniqueID [4]byte // Plugin unique ID
36+
RoutingFlags uint8 // Routing Flags
37+
MixMode uint8 // Mix Mode
38+
GainFactor FXGainFactor // Gain Factor * 10 (9 = 90%, 10 = 100%, 11 = 110%, etc.)
39+
Reserved0B uint8 // Reserved
40+
OutputRouting uint32 // Output Routing (0 = send to master 0x80 + x = send to plugin x)
41+
Reserved10 [16]byte // Reserved
42+
UserPluginName FXUserPluginName // User-chosen plugin name (Windows code page)
43+
LibraryName FXLibraryName // Library name (Original DLL name / DMO identifier - UTF-8 starting from OpenMPT 1.22.07.01, Windows code page in older versions)
44+
DataLength uint32 // Length of plugin-specific data (parameters or opaque chunk)
45+
Data []byte // Plugin-specific data
46+
}
47+
48+
// FourCC returns the big-endian representation of the block identifier
49+
func (b *FX) FourCC() uint32 {
50+
return b.blockBase.FourCC()
51+
}
52+
53+
// Length returns the size of the whole block
54+
func (b *FX) Length() int {
55+
return b.blockBase.Length()
56+
}

music/tracked/it/block/pnam.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package block
2+
3+
import "github.com/gotracker/goaudiofile/internal/util"
4+
5+
type PatternName [32]byte
6+
7+
func (n *PatternName) String() string {
8+
return util.GetString((*n)[:])
9+
}
10+
11+
// PatternNames is a PNAM block
12+
type PatternNames struct {
13+
blockBase
14+
Name []PatternName
15+
}
16+
17+
// FourCC returns the big-endian representation of the block identifier
18+
func (b *PatternNames) FourCC() uint32 {
19+
return b.blockBase.FourCC()
20+
}
21+
22+
// Length returns the size of the whole block
23+
func (b *PatternNames) Length() int {
24+
return b.blockBase.Length()
25+
}

music/tracked/it/flags.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ type IMPMSpecialFlags uint16
6868
const (
6969
// IMPMSpecialFlagMessageAttached :: On = song message attached
7070
IMPMSpecialFlagMessageAttached = IMPMSpecialFlags(1 << 0)
71-
// IMPMSpecialFlagReservedBit1 :: Reserved
72-
IMPMSpecialFlagReservedBit1 = IMPMSpecialFlags(1 << 1)
73-
// IMPMSpecialFlagReservedBit2 :: Reserved
74-
IMPMSpecialFlagReservedBit2 = IMPMSpecialFlags(1 << 2)
71+
// IMPMSpecialFlagHistoryIncluded :: On = history data included (maybe)
72+
IMPMSpecialFlagHistoryIncluded = IMPMSpecialFlags(1 << 1)
73+
// IMPMSpecialFlagHighlightDataIncluded :: On = highlight data included
74+
IMPMSpecialFlagHighlightDataIncluded = IMPMSpecialFlags(1 << 2)
7575
// IMPMSpecialFlagEmbedMidi :: MIDI configuration embedded
7676
IMPMSpecialFlagEmbedMidi = IMPMSpecialFlags(1 << 3)
7777
// IMPMSpecialFlagReservedBit4 :: Reserved
@@ -105,6 +105,16 @@ func (sf IMPMSpecialFlags) IsMessageAttached() bool {
105105
return (sf & IMPMSpecialFlagMessageAttached) != 0
106106
}
107107

108+
// IsHistoryIncluded returns true if there is a history block following the pattern parapointres in the file
109+
func (sf IMPMSpecialFlags) IsHistoryIncluded() bool {
110+
return (sf & IMPMSpecialFlagHistoryIncluded) != 0
111+
}
112+
113+
// IsHighlightDataIncluded returns true if there is a highlight data attached to the file
114+
func (sf IMPMSpecialFlags) IsHighlightDataIncluded() bool {
115+
return (sf & IMPMSpecialFlagHighlightDataIncluded) != 0
116+
}
117+
108118
// IsEmbedMidi returns true if embedded midi configuration is enabled
109119
func (sf IMPMSpecialFlags) IsEmbedMidi() bool {
110120
return (sf & IMPMSpecialFlagEmbedMidi) != 0

music/tracked/it/it.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88

99
"github.com/gotracker/goaudiofile/internal/util"
10+
"github.com/gotracker/goaudiofile/music/tracked/it/block"
1011
)
1112

1213
var (
@@ -24,6 +25,7 @@ type File struct {
2425
Instruments []IMPIIntf
2526
Samples []FullSample
2627
Patterns []PackedPattern
28+
Blocks []block.Block
2729
}
2830

2931
// FullSample is a full sample, header + data
@@ -57,6 +59,7 @@ func Read(r io.Reader) (*File, error) {
5759
Instruments: make([]IMPIIntf, 0),
5860
Samples: make([]FullSample, 0),
5961
Patterns: make([]PackedPattern, 0),
62+
Blocks: make([]block.Block, 0),
6063
}
6164
if err := binary.Read(buffer, binary.LittleEndian, &f.OrderList); err != nil {
6265
return nil, err
@@ -70,8 +73,41 @@ func Read(r io.Reader) (*File, error) {
7073
if err := binary.Read(buffer, binary.LittleEndian, &f.PatternPointers); err != nil {
7174
return nil, err
7275
}
76+
7377
// the earliest valid position to read from
74-
valPos := ParaPointer32(0x00C0 + len(f.OrderList) + len(f.InstrumentPointers)*4 + len(f.SamplePointers)*4 + len(f.PatternPointers)*4)
78+
valPos := ParaPointer32(len(data))
79+
if f.Head.SpecialFlags.IsMessageAttached() {
80+
valPos = ParaPointer32(0x00C0 + len(f.OrderList) + len(f.InstrumentPointers)*4 + len(f.SamplePointers)*4 + len(f.PatternPointers)*4)
81+
}
82+
83+
if f.Head.SpecialFlags.IsHistoryIncluded() {
84+
var historyParaLen uint16
85+
if err := binary.Read(buffer, binary.LittleEndian, &historyParaLen); err != nil {
86+
return nil, err
87+
}
88+
89+
hist := int(historyParaLen)*8 + valPos.Offset() + 2
90+
if hist >= valPos.Offset() && hist < len(data) {
91+
// TODO: read history values
92+
valPos += ParaPointer32(historyParaLen)*8 + 2
93+
}
94+
}
95+
96+
nextValPos := valPos
97+
blockReadLoop:
98+
for {
99+
block, err := readBlock(data, nextValPos, f.Head.TrackerCompatVersion)
100+
if err != nil || block == nil {
101+
break blockReadLoop
102+
}
103+
104+
f.Blocks = append(f.Blocks, block)
105+
nextValPos += ParaPointer32(block.Length())
106+
107+
if nextValPos.Offset() < len(data) {
108+
valPos = nextValPos
109+
}
110+
}
75111

76112
for _, ptr := range f.InstrumentPointers {
77113
if ptr < valPos {

music/tracked/it/readblock.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package it
2+
3+
import (
4+
"bytes"
5+
"encoding/binary"
6+
"io"
7+
8+
"github.com/gotracker/goaudiofile/music/tracked/it/block"
9+
)
10+
11+
func readBlock(data []byte, ptr ParaPointer, cmwt uint16) (block.Block, error) {
12+
ofs := ptr.Offset()
13+
if ofs > len(data)-4 {
14+
return nil, io.EOF
15+
}
16+
r := bytes.NewBuffer(data[ofs:])
17+
18+
var blockID uint32
19+
if err := binary.Read(r, binary.BigEndian, &blockID); err != nil {
20+
return nil, err
21+
}
22+
23+
switch {
24+
case blockID == 0x504E414D: // PNAM
25+
return readBlockPNAM(data, ptr, cmwt)
26+
case blockID>>16 == 0x4658: // FX__
27+
return readBlockFX00(data, ptr, cmwt)
28+
default:
29+
return readBlockUnknown(data, ptr, cmwt)
30+
}
31+
}
32+
33+
func readBlockPNAM(data []byte, ptr ParaPointer, cmwt uint16) (block.Block, error) {
34+
p := block.PatternNames{}
35+
36+
ofs := ptr.Offset()
37+
r := bytes.NewBuffer(data[ofs:])
38+
39+
if err := binary.Read(r, binary.LittleEndian, &p.Identifier); err != nil {
40+
return nil, err
41+
}
42+
43+
if err := binary.Read(r, binary.LittleEndian, &p.BlockLen); err != nil {
44+
return nil, err
45+
}
46+
47+
var nam block.PatternName
48+
cNameLen := len(nam)
49+
50+
for pos := uint32(0); pos < p.BlockLen; {
51+
nlen := int(p.BlockLen - pos)
52+
if nlen > cNameLen {
53+
nlen = cNameLen
54+
}
55+
n := make([]byte, nlen)
56+
if err := binary.Read(r, binary.LittleEndian, &n); err != nil {
57+
return nil, err
58+
}
59+
for len(n) < cNameLen {
60+
n = append(n, 0)
61+
}
62+
copy(nam[:], n)
63+
p.Name = append(p.Name, nam)
64+
pos += uint32(nlen)
65+
}
66+
67+
return &p, nil
68+
}
69+
70+
func readBlockFX00(data []byte, ptr ParaPointer, cmwt uint16) (block.Block, error) {
71+
p := block.FX{}
72+
73+
ofs := ptr.Offset()
74+
r := bytes.NewBuffer(data[ofs:])
75+
76+
if err := binary.Read(r, binary.LittleEndian, &p.Identifier); err != nil {
77+
return nil, err
78+
}
79+
80+
if err := binary.Read(r, binary.LittleEndian, &p.BlockLen); err != nil {
81+
return nil, err
82+
}
83+
84+
if err := binary.Read(r, binary.LittleEndian, &p.PluginType); err != nil {
85+
return nil, err
86+
}
87+
88+
if err := binary.Read(r, binary.LittleEndian, &p.UniqueID); err != nil {
89+
return nil, err
90+
}
91+
92+
if err := binary.Read(r, binary.LittleEndian, &p.RoutingFlags); err != nil {
93+
return nil, err
94+
}
95+
96+
if err := binary.Read(r, binary.LittleEndian, &p.MixMode); err != nil {
97+
return nil, err
98+
}
99+
100+
if err := binary.Read(r, binary.LittleEndian, &p.GainFactor); err != nil {
101+
return nil, err
102+
}
103+
104+
if err := binary.Read(r, binary.LittleEndian, &p.Reserved0B); err != nil {
105+
return nil, err
106+
}
107+
108+
if err := binary.Read(r, binary.LittleEndian, &p.OutputRouting); err != nil {
109+
return nil, err
110+
}
111+
112+
if err := binary.Read(r, binary.LittleEndian, &p.Reserved10); err != nil {
113+
return nil, err
114+
}
115+
116+
if err := binary.Read(r, binary.LittleEndian, &p.UserPluginName); err != nil {
117+
return nil, err
118+
}
119+
120+
if err := binary.Read(r, binary.LittleEndian, &p.LibraryName); err != nil {
121+
return nil, err
122+
}
123+
124+
if err := binary.Read(r, binary.LittleEndian, &p.DataLength); err != nil {
125+
return nil, err
126+
}
127+
128+
p.Data = make([]byte, int(p.DataLength))
129+
if err := binary.Read(r, binary.LittleEndian, &p.Data); err != nil {
130+
return nil, err
131+
}
132+
133+
return &p, nil
134+
}
135+
136+
func readBlockUnknown(data []byte, ptr ParaPointer, cmwt uint16) (block.Block, error) {
137+
p := block.Unknown{}
138+
139+
ofs := ptr.Offset()
140+
r := bytes.NewBuffer(data[ofs:])
141+
142+
if err := binary.Read(r, binary.LittleEndian, &p.Identifier); err != nil {
143+
return nil, err
144+
}
145+
146+
if err := binary.Read(r, binary.LittleEndian, &p.BlockLen); err != nil {
147+
return nil, err
148+
}
149+
150+
if p.Length()+ofs > len(data) {
151+
return nil, io.EOF
152+
}
153+
154+
return &p, nil
155+
}

0 commit comments

Comments
 (0)