Hallo!
EMS mag cool sein, finde ich aber total mit Kanonen auf Spatzen geschossen wenn da maximal 40 KB genutzt werden, sowas passt wie gesagt locker auf den Heap, zumal die Musikstücke ja meistens noch kleiner als 40 KB sind. Ich denke, wenn man das EMS-Feature rausnimmt, wird der Code auch nochmal ein Stück kompakter, was wiederum mehr Speicher für das Spiel erlaubt und beim Code selbst zur Übersicht beiträgt. Klar, dann haben wir die Patterndaten im Heap hängen, Jacke wie Hose sozusagen, aber eben keine eventuellen Probleme mit EMS. Bei mir stürzt der Tracker ab wenn ich ein Musikstück laden will, zudem hab ich auf die schnelle keinen Ton aus dem Ding bekommen.
Die Patterns sind bei diesem Tracker, im Gegensatz zu den meisten anderen, Kanal-weise gespeichert, und man kann sie flexibel kombinieren. Daher reichen dann die maximalen 203 Patterns auch für längere Stücke. Wie Du schon schreibst, 10 Patterns auf 90 Positions verteilt. Ziemlich schlaues Konzept eigentlich.
Wenn Du auch einen Sinn darin siehst, dass man auf EMS verzichtet und die < 40 KB auf dem Heap reserviert würde ich Dich gerne dabei unterstützen, den Code entsprechend umzugestalten.
Ich habe die Laderoutine in Pascal umgesetzt, so dass die Patterns auf den Heap geladen werden. Dabei habe ich nicht den BASIC Code 1:1 übersetzt sondern mich hineingedacht was da speichermäßig passiert und das in Pascal nachgebildet. Die Fehlerabfänge habe ich rausgelassen, weil diese nur zum Tragen kämen, wenn das Musikstück in irgendeiner Weise defekt oder jenseits der Definitionsgrenzen wäre.
Code: Alles auswählen
{$G+} { 286er ASM Anweisungen erlauben }
var
length, nptns, ninstrs: byte;
ptnmap: array[0..203] of byte;
insmap: array[0..30] of byte;
{ die kleinen Arrays ptnmap und insmap sind hier im Datensegment, }
{ die größeren ord und die Patterns selbst auf dem Heap (s.u.) }
{ ptnmap und insmap werden scheinbar nur zum Laden gebraucht, }
{ ich bin mir gerade nur nicht sicher ob es nachteilig ist in }
{ einer Procedure Arrays zu definieren }
type instrument = record
mul1, mul2, lev1, lev2, atd1, atd2, sur1, sur2, wav1, wav2, fbcon: byte;
end;
var
ins: array[0..30] of instrument;
type ord_struc = array[0..255, 0..8] of byte;
{
im Original v(0-8), p(0-255) - hier andersherum,
da sonst eine Byte-für-Byte-Schleife beim Laden
erforderlich wäre.
Es dürfte kein Problem sein, die Variablenreihenfolge
im Programm zu drehen.
}
var
ord_pointer: pointer;
ord: ^ord_struc absolute ord_pointer;
{ Zugriff: ord^[p, v] , siehe auch unten bei patn }
type encpatn = record
hword: word;
lbyte: byte;
end;
{
Die drei Bytes die einen Noteneintrag bilden.
Word als erstes ist praktikabel, da damit mit
nur einer Variable Note, Oktave sowie Instrument
gewonnen werden können, lbyte entspricht el.
}
type patn_struc = array[0..203, 0..63] of encpatn;
var
patn_pointer: pointer;
patn: ^patn_struc absolute patn_pointer;
{
Auf die Patterns, dessen Speicher ich hier auf dem Heap angelegt habe,
kann mittels dieser auf einen Pointer zeigenden Stuktur
so zugegriffen werden:
patn^[(pattern-nummer), zeile].hword oder -.lbyte
Im Original werden die Patterns anders referenziert (modptr o.ä.),
da bliebe aber sowieso noch einiges anzupassen.
}
patn_seg, patn_ofs: word;
procedure loadmod(filenm: string);
{ darf in dieser Form wegen den GetMem's nur einmal aufgerufen werden }
{ es sei denn man gibt den hier reservierten Speicher wieder frei (freemem) }
var
f: file;
a, biggestptn: byte;
begin
assign(f, filenm); reset(f, 1);
{ "Header" }
blockread(f, length, 1); blockread(f, nptns, 1); blockread(f, ninstrs, 1);
{ Pattern / Instrument - Mapper }
blockread(f, ptnmap[0], nptns);
blockread(f, insmap[0], ninstrs);
getmem(ord_pointer, word(length) * 9); { Sequencer auf Heap einrichten + laden }
blockread(f, ord_pointer^, word(length) * 9);
{ Größte Pattern-Nummer aus dem Mapper beziehen }
{ und anhand dessen Speicher reservieren }
biggestptn := 0; for a := 0 to nptns - 1 do
if ptnmap[a] > biggestptn then biggestptn := ptnmap[a];
getmem(patn_pointer, word(biggestptn+1) * 192);
{ seg und ofs für die Assembler Routinen ermitteln, die evtl. benutzt werden }
patn_seg := seg(patn_pointer^); patn_ofs := ofs(patn_pointer^);
{ Patternspeicher nullen und Patterns gemäß Mapper auf den Heap laden }
fillchar(patn_pointer^, word(biggestptn+1) * 192, 0);
for a := 0 to nptns - 1 do blockread(f, patn^[ptnmap[a], 0].hword, 192);
{ Instrumente, Speicher nullen und gemäß Mapper laden }
fillchar(ins[0], 31 * 11, 0);
for a := 0 to ninstrs - 1 do blockread(f, ins[insmap[a]], 11);
close(f);
{ Laden aus Datei erledigt, es bleiben noch Initialisierungen zu tun. }
end;
{ Beispiel, wie man über eine Assembler-Function direkt an die Patterndaten gelangt }
function readnote(ptn, row: word): byte; assembler;
asm
{ Folgende Taktangaben beziehen sich auf 486 }
{ Pattern-Offset berechnen (ptn * 192) }
mov cx, ptn { 1 Takt }
shl cx, 6 { 2 Takte } { -> * 64 }
mov ax, cx { 1 Takt }
add cx, cx { 1 Takt } { -> * 128 }
add cx, ax { 1 Takt } [ -> * 192 }
{ Summe: 6 Takte }
{ Vergleich dazu Berechnung mittels MUL: }
{ mov ax, ptn 1 Takt }
{ mov bx, 192 1 Takt }
{ mul bl 13 - 18 Takte }
{ Summe: 15 - 20 Takte }
{ Row-Offset berechnen }
mov bx, row { 1 Takt }
mov ax, bx { 1 Takt }
add bx, bx { 1 Takt } { -> * 2 }
add bx, ax { 1 Takt } { -> * 3 }
{ Summe: 4 Takte }
{ Pattern-Offset zu Row-Offset addieren }
add bx, cx { 1 Takt } { bx enthält nun korrekten Offset }
{ Pointer einlesen }
mov es, patn_seg { 1 Takt }
mov si, patn_ofs { 1 Takt }
{ les si, patn_pointer ginge auch, braucht aber 6 Takte }
{ Byte lesen und Notenwert gewinnen }
mov al, es:[si+bx] { 1 Takt }
shr al, 4 { 2 Takte }
{ Funktion gibt den Wert in AL zurück }
{ Gesamte Function also 16 Takte, plus ggf. Eintritts/Austritts-Vorgänge }
end;
begin
end.
So, das war jetzt etwas viel auf einem Haufen, aber es könnte ein paar Deiner Fragen klären.
Zuerst noch zu der Assembler-Routine: Die besteht ja zu fast 70% aus der Berechnung des Offsets in die Patterndaten.
Man kann es auch in Pascal halten:
Code: Alles auswählen
function readnote(ptn, row: byte): byte;
begin
readnote := patn^[ptn, row].hword shr 12;
end;
Zugegebenermaßen - bedeutend kürzer und wahrscheinlich auch schneller. Ich hatte nur mal Lust auf ein bisschen Assemblergefummel.
Wenn man Speicher auf einen Pointer reserviert kann man mit mem[seg(pvar^):ofs(pvar^)+x] darauf zugreifen, bequemer ist aber wie in meinem obigen Code die Definition einer Datenstruktur mittels Type, und dann eine Variable von diesem Typ die man mit ABSOLUTE auf den Pointer legt. Ich muss da bislang auch immer noch nachgucken wie das genau geht, aber es steht ja oben im Code.
Ein Pointer ist eine 32 Bit Variable und enthält Segment und Offset, ja.
Meines Wissens zeigt ein Pointer immer fest auf einen Speicherort, und "Pointer^" zeigt auf das erste Element des zugewiesenen Speichers. Siehe in meiner Laderoutine, da wird Blockread oder Fillchar auf "Pointer^" angesetzt, wie auf Variablen oder Arrays. Ich kann es gerade nicht prüfen, aber addr(Pointer) ist denke ich nicht das gleiche wie Pointer^, addr liefert die Adresse einer Variablen oder Routine, also wiederum einen Pointer, wenn ich nicht irre (sorry für die Unklarheit).
Zu Deiner Idee mit dem Array: Das wird auch im Code oben gemacht. Es werden soundsoviel Bytes reserviert auf einen Pointer, auf die dann mit einer Array-Struktur zugegriffen werden kann in der Form hier ord^[p, v]. Zugriff wie auf ein normales Array, nur mit ^ zwischen Name und Klammern.
Thomas hat geschrieben:
in Pascal "emulieren" ?
Das sind maximal 203 Byte, daher habe ich es statisch angelegt, selbst wenn es nur einmal in der Laderoutine gebraucht wird. Man könnte es auch über eine Datenstruktur und Pointer und GetMem einrichten und dann mit FreeMem wieder lösen, aber da muss man den Aufwand abwägen, ob der sich lohnt um 203 "Leichenbytes" zu vermeiden.
Auf der anderen Seite ist im Originalcode "ord" statisch definiert mit insgesamt 9 * 255 Bytes, ich richte es nur so groß wie nötig ein. Type Definitionen belegen keinen (nennenswerten) Platz, man könnte auch ein solch als Type definiertes Array 65000 Byte groß machen, auch wenn man nur 255 nutzen will.
Nein ich habe den Soundblaster Pro gehabt, dann eine GUS, dann Soundblaster 64 Gold wenn ich mich recht entsinne.
Bei dem Tracker an sich interessiert mich im Moment eigentlich nur, die Lade- und Abspielroutinen nach Pascal zu portieren, und wenn Du einverstanden bist, ohne EMS. Mit DirectQB hatte ich bisher nicht zu tun...
Es wäre ganz nett, wenn auch ich am Ende eine Unit hätte die PIS abspielen kann.