Skip to content

Commit c28e9e9

Browse files
committed
Initial public release
0 parents  commit c28e9e9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+4673
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
testdata
2+
bin
3+
.idea
4+
analyses/
5+
/rekordbox/
6+
/output/

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
rex:
2+
go build -o bin/rex cmd/rex/*.go
3+

README.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# rex: rekordbox exporter
2+
3+
Open source mixing or library software should be able to create Rekordbox
4+
compatible export files, so that they can be played on Pioneer equipment in
5+
venues all over the world.
6+
7+
This project is my attempt at getting closer to this goal, and leans heavily on the work done by others.
8+
A good starting point is: https://djl-analysis.deepsymmetry.org/rekordbox-export-analysis/
9+
10+
## Project state
11+
12+
It is possible to create PDB files that can be opened in Rekordbox.
13+
I haven't been able to test them on real Pioneer hardware yet.
14+
Trying to import them on a Denon Prime 4 results in something happening, but no library.
15+
16+
Do not use files generated from this project on a live gig, it probably won't work and you'll be miserable.
17+
18+
I figured out some more fields from various tables, and also a bit how the table structure should be built up.
19+
20+
The important stuff is in the [rekordbox package](pkg/rekordbox) subdirectories.
21+
Especially the stuff in [dbengine](pkg/rekordbox/dbengine) and [page](pkg/rekordbox/page)
22+
might be of particular interest.
23+
24+
Many tests are broken, they might not be relevant.
25+
26+
## Tools and development
27+
28+
This software has been tested successfully with Go 1.20.
29+
30+
Use [REX](cmd/rex/main.go) to generate PDB files:
31+
```
32+
go build -o rex cmd/rex/main.go
33+
./rex -root /path/to/USB -scan /path/to/USB/mymusic
34+
```
35+
36+
Use [Analyze](cmd/analyze/main.go) to introspect what's going on inside the files:
37+
```
38+
go build -o analyze cmd/analyze/main.go
39+
./analyze -index -rows /path/to/USB/PIONEER/rekordbox/export.pdb
40+
```

cmd/analyze/main.go

+270
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
package main
2+
3+
import (
4+
`bytes`
5+
`encoding/binary`
6+
`flag`
7+
`fmt`
8+
`io`
9+
`os`
10+
`strconv`
11+
12+
`github.com/ambientsound/rex/pkg/rekordbox/color`
13+
`github.com/ambientsound/rex/pkg/rekordbox/column`
14+
`github.com/ambientsound/rex/pkg/rekordbox/dbengine`
15+
`github.com/ambientsound/rex/pkg/rekordbox/page`
16+
`github.com/ambientsound/rex/pkg/rekordbox/track`
17+
`github.com/ambientsound/rex/pkg/rekordbox/unknown17`
18+
`github.com/ambientsound/rex/pkg/rekordbox/unknown18`
19+
)
20+
21+
/*
22+
This program analyzes a PDB file block by block. That is, each block is parsed separately and not put together into a larger structure.
23+
This means that all kinds of files, also corrupt (mis-generated) can be inspected.
24+
*/
25+
26+
var (
27+
printIndex = flag.Bool("index", false, "print contents of index structure")
28+
printRows = flag.Bool("rows", false, "print individual rows")
29+
dumb = flag.Bool("dumb", false, "don't attempt to parse tables")
30+
)
31+
32+
func main() {
33+
err := run()
34+
if err != nil {
35+
fmt.Printf("fatal error: %s\n", err)
36+
os.Exit(1)
37+
}
38+
}
39+
40+
func run() error {
41+
flag.Parse()
42+
43+
f, err := os.Open(flag.Args()[0])
44+
if err != nil {
45+
return err
46+
}
47+
defer f.Close()
48+
49+
if *dumb {
50+
return run_ordered(f)
51+
}
52+
return run_parser(f)
53+
}
54+
55+
func run_parser(f io.ReadWriteSeeker) error {
56+
57+
db, err := dbengine.Open(f)
58+
if err != nil {
59+
return err
60+
}
61+
62+
fmt.Printf("PIONEER DJ DeviceSQL file\n")
63+
fmt.Printf("tables=%d, tx=%d\n", db.Globals.NumTables, db.Globals.Sequence)
64+
65+
types := db.TableTypes()
66+
for _, ty := range types {
67+
table, err := db.GetTable(ty)
68+
if err != nil {
69+
return err
70+
}
71+
tableName := ty.String()[5:]
72+
fmt.Printf("Table: %q\n", tableName)
73+
fmt.Printf(
74+
" Meta: u1=%04x u2=%04x numentries=%02d\n",
75+
table.Index.IndexHeader.Unknown1,
76+
table.Index.IndexHeader.Unknown2,
77+
table.Index.IndexHeader.NumEntries,
78+
)
79+
80+
totalRows := 0
81+
totalActive := 0
82+
totalRowSets := 0
83+
84+
if *printIndex && table.Index.NumEntries > 0 {
85+
fmt.Printf(" Indexes:")
86+
for i := range table.Index.IndexEntries {
87+
fmt.Printf(" %04x", table.Index.IndexEntries[i])
88+
}
89+
fmt.Printf("\n")
90+
}
91+
92+
for _, pg := range table.Pages {
93+
lr := strconv.FormatUint(uint64(pg.NumRowsLarge), 2)
94+
fmt.Printf(" %s Page: idx=%02x rows=%3d deleted=%3d used=%4d free=%4d large=%010s tx=%04x flags=%02x u3=%04x u4=%04x u5=%04x\n",
95+
tableName,
96+
pg.Header.PageIndex,
97+
pg.Header.NumRowsSmall,
98+
int(pg.Header.NumRowsSmall)-pg.ActiveRows(),
99+
pg.Header.NextHeapWriteOffset,
100+
pg.Header.FreeSize,
101+
lr,
102+
pg.Header.Transaction,
103+
pg.Header.PageFlags,
104+
pg.Header.Unknown3,
105+
pg.Header.Unknown4,
106+
pg.DataHeader.Unknown5,
107+
)
108+
109+
totalRows += int(pg.Header.NumRowsSmall)
110+
totalActive += pg.ActiveRows()
111+
totalRowSets += len(pg.RowSets)
112+
113+
if !*printRows {
114+
continue
115+
}
116+
117+
for _, rs := range pg.RowSets {
118+
bm := strconv.FormatUint(uint64(rs.ActiveRows), 2)
119+
pd := strconv.FormatUint(uint64(rs.LastWrittenRows), 2)
120+
// an := strconv.FormatUint(uint64(rs.ActiveRows&rs.LastWrittenRows), 2)
121+
// xo := strconv.FormatUint(uint64(rs.ActiveRows^rs.LastWrittenRows), 2)
122+
fmt.Printf(" RowSet: bitmask=%016s lastwrite=%016s\n", bm, pd)
123+
}
124+
125+
for rowNum, rowref := range pg.HeapPositions() {
126+
if table.Type == page.Type_Tracks {
127+
128+
tr := &track.Track{}
129+
err = pg.UnmarshalRow(tr, rowref.HeapPosition)
130+
if err != nil {
131+
fmt.Printf(" Track: io.EOF\n")
132+
} else {
133+
fmt.Printf(" Track: heap=%04x id=%04x shift=%02x exists=%-5v path=%q\n", rowref.HeapPosition, tr.Id, tr.IndexShift, rowref.Exists, tr.FilePath)
134+
}
135+
} else if table.Type == page.Type_Unknown18 {
136+
row := &unknown18.Unknown18{}
137+
err = pg.UnmarshalRow(row, rowref.HeapPosition)
138+
fmt.Printf(" %#v\n", row)
139+
} else if table.Type == page.Type_Columns {
140+
row := &column.Column{}
141+
err = pg.UnmarshalRow(row, rowref.HeapPosition)
142+
fmt.Printf(" %04x %#v\n", rowref.HeapPosition, row)
143+
} else if table.Type == page.Type_Colors {
144+
row := &color.Color{}
145+
err = pg.UnmarshalRow(row, rowref.HeapPosition)
146+
fmt.Printf(" %04x %#v\n", rowref.HeapPosition, row)
147+
} else if table.Type == page.Type_Unknown17 {
148+
row := &unknown17.Unknown17{}
149+
err = pg.UnmarshalRow(row, rowref.HeapPosition)
150+
fmt.Printf(" %#v\n", row)
151+
} else {
152+
fmt.Printf(" Row: index=%03d heap=%04x exists=%v\n", rowNum, rowref.HeapPosition, rowref.Exists)
153+
}
154+
}
155+
}
156+
157+
fmt.Printf(" Table summary: records=%d deleted=%d total=%d rowsets=%d\n", totalActive, totalRows-totalActive, totalRows, totalRowSets)
158+
}
159+
160+
return nil
161+
}
162+
163+
func run_ordered(f io.ReadWriteSeeker) error {
164+
flag.Parse()
165+
166+
const blocksize = 4096
167+
buf := make([]byte, blocksize)
168+
169+
db, err := dbengine.Open(f)
170+
if err != nil {
171+
return err
172+
}
173+
174+
fmt.Printf("%05x: numtables=%d, next_unused_page=%05x, sequence=%d\n", 0, db.Globals.NumTables, db.Globals.NextUnusedPage*blocksize, db.Globals.Sequence)
175+
176+
for _, ptr := range db.Globals.Pointers {
177+
fmt.Printf("%-20s first=%02x last=%02x empty_candidate=%02x\n", ptr.Type.String()[5:], ptr.FirstPage, ptr.LastPage, ptr.EmptyCandidate)
178+
}
179+
180+
blanks := 0
181+
i := 1
182+
_, err = f.Seek(blocksize, io.SeekStart)
183+
if err != nil {
184+
return err
185+
}
186+
187+
for ; ; i++ {
188+
_, err = io.ReadAtLeast(f, buf, len(buf))
189+
if err == io.EOF {
190+
break
191+
}
192+
if err != nil {
193+
return err
194+
}
195+
196+
header := &page.Header{}
197+
idxheader := &page.IndexHeader{}
198+
r := bytes.NewReader(buf)
199+
200+
err = binary.Read(r, binary.LittleEndian, header)
201+
if err != nil {
202+
return err
203+
}
204+
err = binary.Read(r, binary.LittleEndian, idxheader)
205+
if err != nil {
206+
return err
207+
}
208+
209+
// isIndex := header.PageFlags & 0x64
210+
211+
if header.Type == 0 && header.PageIndex == 0 {
212+
fmt.Printf("%05x: NO DATA\n", i*blocksize)
213+
blanks++
214+
continue
215+
}
216+
217+
fmt.Printf("%05x: idx=%02x next=%05x seq=%d type=%-16s",
218+
i*blocksize,
219+
header.PageIndex,
220+
header.NextPage*blocksize,
221+
header.Transaction,
222+
header.Type.String()[5:],
223+
)
224+
225+
switch header.PageFlags {
226+
case 0x64:
227+
fmt.Printf(" <INDEX>")
228+
if !*printIndex {
229+
fmt.Printf("\n")
230+
continue
231+
}
232+
if idxheader.NumEntries == 0 {
233+
fmt.Printf(" <EMPTY>\n")
234+
continue
235+
}
236+
fmt.Printf(
237+
" entries=%d u1=%04x u2=%04x break=%04x\n",
238+
idxheader.NumEntries,
239+
idxheader.Unknown1,
240+
idxheader.Unknown2,
241+
idxheader.FirstEmptyEntry,
242+
)
243+
for idxnum := 0; idxnum < int(idxheader.NextOffset); idxnum++ {
244+
var unknown uint32
245+
err = binary.Read(r, binary.LittleEndian, &unknown)
246+
if err != nil {
247+
return err
248+
}
249+
// if unknown == 0x1ffffff8 {
250+
// break
251+
// }
252+
s := strconv.FormatUint(uint64(unknown), 2)
253+
fmt.Printf("> pos=%02d dec=%04d hex=%08x bin=%032s\n", idxnum, unknown, unknown, s)
254+
}
255+
case 0x37:
256+
fmt.Printf(" <XXX>")
257+
fallthrough
258+
case 0x34:
259+
fmt.Printf(" <REF>")
260+
fallthrough
261+
case 0x24:
262+
fmt.Printf(" <DATA>")
263+
fmt.Printf(" rows=%d\n", header.NumRowsSmall)
264+
default:
265+
panic("cannot handle it")
266+
}
267+
}
268+
269+
return nil
270+
}

0 commit comments

Comments
 (0)