Trackermodul-Engine (sehr einfach)

Diskussion zum Thema Programmierung unter DOS (Intel x86)
wobo
DOS-Guru
Beiträge: 614
Registriert: So 17. Okt 2010, 14:40

Re: Trackermodul-Engine (sehr einfach)

Beitrag von wobo »

Schöner Thread – ich konnte zwar noch nicht alles lesen. Aber mit Schleifen meinst Du das Loopen eines Samples (ich habe bisher nie verstanden, was Du mit den Schleifen konkret meinst)? Ich denke nicht, dass das wild ist. Du musst eh irgendwann die Lauflänge des Samples abfragen/behandeln. Ist die Lauflänge überschritten, dann beginnst du halt wieder von vorn. Diese Abfrage ist auch nicht zeitintensiv, weil Du sie ja nur machen musst, wenn die Samplelänge überschritten ist und die musst Du eh abfragen.

Wegen der Geschwindigkeit würde ich mir keine Sorgen machen. Mein Player konnte auf einem 386sx16 gerade einmal mit Ach und Krach 4 channels bei 16kHz vermixen. Ich habe dort eine CPU-Auslastung von ca. 90%. Auf demselben Rechner vermixt z.B. Inertia Player 8 channels bei 44 kHz und hat noch ordentlich Luft nach oben (was man allein schon an der Bedienbarkeit der Benutzeroberfläche etc. merkt). Auf einem P150 jedoch ist es nahezu vollkommen egal, ob mein Player während eines Games noch mitläuft oder nicht. Derselbe Player nimmt dort nur noch 4% der CPU in Anspruch. Bezogen auf einen Pentium 60 macht das vielleicht den Unterschied aus, ob man einen P60 oder einen P66 braucht. Und das ist wirklich keine Sache, worüber ich mir Gedanken machen würde.

Das einzige Problem, das ich bei Sample-Schleifen hatte, war ein Format-Konventions-Problem. Beispiel: Samplelänge von 22 byte, zuletzt gespielte Sample-Position = 20, Schrittweite wegen aktueller Tonhöhe = 3,25. Ergo ergäbe sich als neue Sample-Position 23,25 (abgerundet auf 23). Muss ich beim Loopen mit Sample-Position 0 beginnen, oder mit Position 1,25 (abgerundet auf 1)?

Ich habe damals beide Versionen programmiert, aber leider kein eindeutiges Ergebnis erhalten. Entweder hatte bei dem einem MOD das Wiederbeginnen fix bei Null schief geklungen, während bei dem anderen ein positionsabhängiges Loopen schief geklungen hat. (Mein Fehler lag also woanders – wahrscheinlich bei den verflixten Effekten) Bei größeren Samples (>500 byte) war es für meine Ohren jedoch total egal, ob beim Loopen immer fix mit Null oder positionsabhängig begonnen wurde.

Als Testsong würde ich doch eher was kurzes machen, wie z.B. den Addams-Familiy-Theme-Song (https://www.youtube.com/watch?v=X6QzbvH-ZNo) und Gesang durch irgendwas anderes ersetzen. Aber mach am Besten, worauf Du am meisten Lust hast!
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Hallo wobo!

Stimmt, ein Sample mit Schleife verhält sich wie ein Sample ohne, nur dass statt am Ende aufhören
wieder zur Schleifen-Anfangsposition gesprungen wird. Diese liegt aber nicht immer bei null, sonden
kann überall zwischen Anfang und Ende des Samples definiert sein.

Nunja, ich wollte sowieso erstmal den Player in reinem Pascal schreiben, und wenn alles funktioniert
dann ich ASM umsetzen.

Bei einem Sample mit 22 geloopten Bytes müsste man bei erreichen von 23,25 auf 1,25 wechseln,
aber ohne Runden, das Komma muss bleiben, sonst klingt es zusammen mit anderen Noten schief.
Ja und bei größeren Samples wird die Tonhöhe selber nicht durch die Schleife definiert, sondern
diese wiederholt eben nur einen Teil.

Aber was macht man wenn man ein Sample von 8 Byte hat und die Schrittweite wäre 10?
Das wäre natürlich ein ziemlich unwahrscheinlicher Fall und akustisch würde das nur
Aliasing produzieren. Aber vielleicht würde es helfen, eine generelle Routine für die
Schleifen zu finden, die immer funktioniert.

Ja, ich muss was trackern was mir Spass macht. Ist immerhin doch ne Arbeit von mehreren Stunden
so einen Songs zu transferieren. Je nach Anspruch...
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Mal ne Frage, der ich aktuell gegenüberstehe:
Gibt es eine Möglichkeit, in Assembler innerhalb Pascal einem 32 Bit Register ohne große Umwege
einen Longint zu übergeben? Pascal meckert natürlich wenn ich schreibe d66h; mov ax, longintvariable,
denn er kennt das 66h ja nicht.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
Dosenware
DOS-Gott
Beiträge: 3745
Registriert: Mi 24. Mai 2006, 20:29

Re: Trackermodul-Engine (sehr einfach)

Beitrag von Dosenware »

evtl. könnte dir eine Hilfsvariable mit absolute weiterhelfen.

var x:word;
var y:array[0..1] of byte absolute x;

bewirkt das dass array y auf der Adresse von x liegt, und man so byteweise darauf Zugreifen kann - damit könnstest du das evtl. auch lösen...
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Danke, das mit der Hilfsvariable die per absolute auf die eigentliche Variable zeigt ist wohl der richtige Weg.
Ich habe es jetzt so gelöst, kleines Testprogrämmchen:

Code: Alles auswählen

{$G+}
var
   long1, long2: longint;
   foreax: word absolute long1;
   foredx: word absolute long2;
begin

  long1 := 100000;
  long2 := 200000;

  asm
    db 66h; mov ax, foreax
    db 66h; mov dx, foredx
    db 66h; add ax, dx
    db 66h; mov foreax, ax
  end;

  writeln(long1); (* raus kommt 300000, funktioniert also *)

end.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
Dosenware
DOS-Gott
Beiträge: 3745
Registriert: Mi 24. Mai 2006, 20:29

Re: Trackermodul-Engine (sehr einfach)

Beitrag von Dosenware »

probiere lieber komplexere Zahlen um sicherzugehen z.b. 1234567+2345678=3580245
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Funktioniert auch :)
mov ax, 13h
int 10h

while vorne_frei do vor;
DOSferatu
DOS-Übermensch
Beiträge: 1220
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

zatzen hat geschrieben:Mal ne Frage, der ich aktuell gegenüberstehe:
Gibt es eine Möglichkeit, in Assembler innerhalb Pascal einem 32 Bit Register ohne große Umwege
einen Longint zu übergeben? Pascal meckert natürlich wenn ich schreibe d66h; mov ax, longintvariable,
denn er kennt das 66h ja nicht.
Das 66h ist nicht das Problem. Byte-einfügen geht ja immer.
Man muß hier quasi "Type-casten".
Der Befehl lautet ja immer noch MOV AX,irgendwas und irgendwas muß ein WORD sein. (das 66h macht dann den 32bit Befehl draus.

Gemacht wird es so:
db 66h; mov AX, word ptr longintvariable

So meckert er nicht, weil er "denkt", man will nur das untere Word der Longintvariable haben. Daß das db 66h das Ganze zu einem
mov EAX, longintvariable
macht, weiß Pascal (bzw der TASM) ja nicht, für den ist das nur ein Byte, das "aus irgendwelchen Gründen" davorsteht.

Achja, btw für Sachen, die für einen Tracker gut umzusetzen wären:
geradezu ideal (weil selbst schon "Synthesizer wie sonstwas") ist "Axel F." von Harold Faltermeyer. (Das Theme von "Beverly Hills Cop", falls DAS etwa jemand nicht weiß...)

Mir würde z.B. auch Titelmusik oder Ending der Serie (oder das Ending des Films) von Max Headroom gefallen - auch alles sehr synthesizer-tauglich.
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Oh sehr gut, dann brauch ich ja nicht mal eine weitere Variable.

Hier mal die aktuelle Version der Bitread-Routine. Ich hatte mal gemessen dass die in
Assembler viel schneller ist als wenn man die Rechnerei in Pascal Code umsetzt:

Code: Alles auswählen

function readbits(length: byte): byte; assembler;
asm

  les si, patterns_pointer

  (* 32 Bit Bitposition in BX einlesen und auch in DX uebergeben *)
  db 66h; mov bx, word ptr bitread_pos { mov ebx, bitread_pos }
  db 66h; mov dx, bx  { mov edx, ebx }

  (* EBX durch 8 teilen, WORD einlesen *)
  db 66h; shr bx, 3 { shr ebx, 3 }
  mov ax, es:[si+bx]

  (* Modulo Wert erzeugen bzw. in DL steht hiernach der Bitoffset *)
  db 66h; shl bx, 3  { shl ebx, 3 }
  db 66h; sub dx, bx { sub edx, ebx }

  (* zurechtschieben, Bitoffset elimieren *)
  mov cl, dl
  shr ax, cl
  (* zu lesende Bits befinden sich nun nur noch in AL *)


  (* links ueberzaehlige Bits rausschieben *)
  mov dh, length
  mov cl, 8
  sub cl, dh
  shl al, cl
  shr al, cl

  (* bitposition weiterzaehlen *)
  db 66h; xor bx, bx { xor ebx, ebx }
  mov bl, dh
  db 66h; add word ptr bitread_pos, bx { add bitread_pos, ebx }
end;
Also ich noch nicht diese 32 Bit Variablen-Möglichkeit hatte musste ich mir das mit
der Bitposition (die ja immer 8 mal so große Werte erreicht wie die Byteposition)
vorher in mehrere Variablen aufdröseln.

Wahrscheinlich kann man an der Routine hier noch was drehen, wie war das noch
mit der Geschwindigkeitsoptimierung, dass man bestimme Zeilen woanders hin
dazwischenschiebt weil manche Registerzugriffe langsam sind? Und dass irgendwie
LEA [ ... + ... ] schneller ist als der sonstige Befehlssatz?
Ich hatte jetzt ca. 18 Monate kein Assembler mehr programmiert, da vergisst
man schonmal was. Ich bin erstmal froh dass ich diese Routine überhaupt so wie
sie ist vorhin noch neu geschrieben und komplett verstanden habe.


Was die Musik angeht, vielleicht mach ich auch "Rockit" von Herbie Hancock...
Da wäre es nur wirklich toll wenn ich die original Samples hätte.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Ich hab dermaßen ein Brett vorm Kopf was jetzt beim Player das Befüllen der Puffer angeht.
Das Problem ist dass die Zeilenlängen ja anders lang sind als der Puffer. Einen Monophonen
Kanal hab ich schon hinbekommen, aber mehrere Spuren nacheinander und das je Zeile...
Kann mir vielleicht jemand helfen?

Axel F müsste übrigens klar gehen, da kann man die meisten Sound aus dem Extended Mix herausholen
und selbigen damit nachbauen.

EDIT: Okay, ich komme der Sache schon näher. Innerhalb einer Pufferlänge stimmt das Timing
mit der Polyphonie, nur beim Wechsel au den nächsten Puffer gibts noch Chaos.
mov ax, 13h
int 10h

while vorne_frei do vor;
DOSferatu
DOS-Übermensch
Beiträge: 1220
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

zatzen hat geschrieben:Ich hab dermaßen ein Brett vorm Kopf was jetzt beim Player das Befüllen der Puffer angeht. [...]
OK. Ich weiß nicht, ob es eine Hilfe ist - aber ich sage einfach mal, wie ich es gemacht habe (weil es bei mir funktioniert).
Wie Du ja weißt, nutze ich keine Patterns, die die Daten/Noten für alle Stimmen gleichzeitig enthalten, sondern "programmiere" die Stimmen unabhängig voneinander.
Damit das funktioniert, arbeitet der "Sounddatengenerator" (die ISM) mit einer Art "Multitasking":
Zu Anfang lösche ich den Puffer (klar).
Dann werden die Daten für Stimme 0 da hinein addiert (sofern Stimme 0 aktiv) - und der "aktive_Stimmen" Zähler +1
Dann werden die Daten für Stimme 1 da hinein addiert (sofern Stimme 1 aktiv) - und der "aktive_Stimmen" Zähler +1
...
Dann werden die Daten für Stimme 15 da hinein addiert (sofern Stimme 15 aktiv) - und der "aktive_Stimmen" Zähler +1
Das heißt, die Stimmen werden immer "im Kreis" generiert, 16 Durchläufe pro Puffer (wobei inaktive Stimmen natürlich nur einen kurzen Durchlauf haben).

Am Schluß wird der "aktive_Stimmen" Zähler und die gesetzte MasterVolume mit den zuletzt gespeicherten Werten verglichen - weicht einer ab, wird eine neue Tabelle generiert (die den Sound "hochdreht" ohne zu clippen).
Und der ganze Puffer wird immer mit dieser Tabelle "behandelt".
OK - das ist bekannt. Wollte es nur nochmal erwähnen, um klarzustellen, wie der ganze Vorablauf ist.

Damit das Ganze aber funktioniert, wird der "Status" der jeder Stimme in einem Puffer festgehalten. Dieser wird, wenn der Generator für eine Stimme startet, dort ausgelesen und in die Register geworfen. Und wenn die Stimme fertig generiert ist, dann wieder in den Puffer. Mit "Status" meine ich damit JEDEN Zähler und Modus usw.
Das heißt: Auch wenn der Generator beim Generieren der Daten für eine Stimme quasi "mitten in einer Note" aufhören muß (weil der Puffer zu Ende ist), kann er beim Generieren des nächsten Puffers wieder da anfangen, wo er zuletzt aufgehört hat. Das heißt: es gibt keine Fehler beim Übergang, also keine "kaputten Nahtstellen" am Anfang oder Ende eines generierten Sounddatenpuffers.

Anmerkung: Selbst wenn ich mit Patterns, die Daten für alle Stimmen gleichzeitig (wie bei MOD-artigen Sounddaten) hätte, wäre ich trotzdem nie auf die Idee gekommen, Patterns und Puffer zu "synchronisieren", d.h. die Puffergröße abhängig von den Patterns zu machen.

Ich würde trotzdem immer davon ausgehen, daß die Größe des Ringpuffers (und der Subpuffer darun) für die Sounddaten generell außerhalb vom Programmierer festgelegt werden können und sollen - nicht vom Generator. Der Generator hat nur auf Anforderung den bereitgestellten Puffer bis zum Rand mit Daten zu füllen - wie er das anstellt, hat das Hauptprogramm nicht zu interessieren.

Die Stimmen-Puffer sind bei mir pro Stimme 96 Bytes groß - für alle Stimmen zusammen also 1,5 kB.
(Ich habe aus naheliegenden Gründen eine /16 teilbare Größe gewählt.)
Die ersten 32 Bytes enthalten eine generierte Welle (die mit dem WAV Befehl aus kombinierten Einzelwellen generiert wird), d.h. das ist sozusagen ein 32-Byte-Sample, das in Schleife immer wieder abgespielt wird (mit entsprechender Frequenz), solange die Note läuft.
Dann folgt ein 8-Byte-Stack (4 Words), der hauptsächlich dazu gedacht ist, "Unterprogramme" zu ermöglichen, jedoch auch zum Zwischenspeichern der "Register" benutzt werden kann. (Ich biete zwei 8-bit Register, die primär als Zähler für Schleifen gedacht sind, aber mit dem Block von Spezialbefehlen auch für komplexere Berechnungen und "Modifikationen" der Sounddaten benutzt werden können.)
Danach folgen einige Counter (Zähler), die für Notenlänge, Position im Wave/Sample, Position in der Hüllkurve und Position beim Bending (Portamento) gedacht sind (das sind u.a. die Dinge, die die Stimme speichert und wieder lädt).
Dann kommen natürlich noch Daten wie die aktuelle Position in den Sounddaten und verschiedene Flags. Schließlich folgen noch die Daten, die durch den User gesetzt werden und die sich nur verändern, wenn sie ein neuer Befehl ändert - als da wären Hüllkurve, Lautstärke, Welle/Samplenr, Effekte. Ganz am Ende stehen dann die beiden (obengenannten) Register X und Y.
D.h. jede der Stimmen hat quasi eine "Registerbank" von 96 Bytes - das Ganze ist irgendwie aufgebaut wie eine virtuelle Maschine mit 16 Tasks.

@zatzen: Ich hoffe, ich konnte Dir damit einige Anregungen geben. Die Hauptidee ist eben: Am Ende des Puffergenerierens den kompletten Status sichern und vor dem Generieren des nächsten Puffers zurückholen. Ja, ich weiß, dadurch vergehen bestimmt ein paar Mikrosekunden oder Nanosekunden mehr am Anfang und Ende. Dafür kann man aber sauber und fehlerfrei Puffer beliebiger Größe generieren.
Dieses "initialisieren" (und das Sichern am Ende) ist auch der Grund, wieso die Puffer nicht allzu klein werden sollten-
Zu große Puffer hingegen führen zu einem hörbaren "Versatz" vom Sound zur Grafik.

Übrigens ist die Figurensteuerung, dieses GameSys2 von mir, ebenfalls wie eine virtuelle Maschine aufgebaut - allerdings mit bis zu über 2000 Tasks und mit variablen Stacks (Registerbänken) für jeden Task. Dieser wird beim Anlegen eines Tasks (einer Figur) in einem Stack reserviert und beim Entfernen des Tasks (der Figur) wieder freigegeben (dynamische Speicherverwaltung).

In den letzten Jahren programmiere ich bei größeren Projekten fast nur noch nach diesem Konzept - große Datenmengen und komplexere Daten sind so für mich einfach am besten zu erfassen.
Wenn ich da so an meine ersten Spiele (auf dem KC 85/1 in BASIC) denke, wie ich da wie ein Dusseliger mit IF-Kaskaden rumgemurkst habe... - oh weh... Aber, na gut - wir waren schließlich alle mal jung.

Ich hätte auch mal wieder Lust, ein kleines Spiel zu schreiben, einfach mal so. Aber so wie ich inzwischen drauf bin, artet jedes kleine Projekt immer gleich in solche Dinge aus. Was daran wirklich schade ist, ist, daß man sich in der Entwicklung solcher Sachen quasi dermaßen "verzettelt", daß das eigentlich geplante Projekt dabei immer mehr in den Hintergrund tritt. Ich habe auch seit Wochen nichts anständiges mehr programmiert, weil ich immer nur müde und genervt vom Job bin.
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Da ich eher so nebenbei programmiere, also nur alle paar Monate mal für einige Wochen Lust drauf habe,
und weil ich das jetzt so einschätze, stecke ich mir meine Ziele auch entsprechend anders. So würde ich
was ein Spiel angeht jetzt auch wieder mehr in Richtung simpleres Jump&Run gehen. Überhaupt einfach
mal etwas machen um ZVID und ZSTM einzusetzen.
Das ist natürlich verglichen mit dem was möglich wäre total banal und untertalentiert, aber dafür dass
ich so wenig programmiere macht es mir dann irgendwie nur Spaß, indem ich dann solche ungewöhnlichen
Formate verwende und da auch Programmierarbeit reinstecke.
Wenn ich einfach nur ein Spiel umsetzen wollte, sagen wir ein Adventure, und ich hätte die ganzen Grafiken
und ne gute Story - dann würde ich wahrscheinlich sowas wie AGS nehmen und mir das Programmieren sparen.
Aber ganz im Gegenteil, ich habe schon Spaß daran einen eigenen Musikplayer zu machen, und als der
jetzt vor ein paar Tagen endlich funktionierte hab ich mich richtig gefreut.

Befüllen des Puffers:
Ja, ich glaube das war der Knackpunkt: Daten für alle Stimmen gleichzeitig, d.h. auf einer Patternzeile.
Ich muss ja eine Zeile einlesen, die für alle meiner 8 Stimmen die Paramenter neu setzt, und dann
müssen diese 8 Stimmen der Geschwindigkeit wegen nacheinander in den Puffer addiert werden.
Das habe ich jetzt so gelöst:

Code: Alles auswählen

procedure fill_soundbuffer;
  var
    fill_length: word;
    buffercountdown: word;
    track: byte;
  label
    skip;
begin

  fillchar(soundbuffer^, soundbuffer_size * 2, 0);

  mix_position := 0;
  buffercountdown := soundbuffer_size;

  if row_countdown > soundbuffer_size then
  begin
    fill_length := soundbuffer_size;
    mix_in_channels(fill_length);
    dec(row_countdown, fill_length);
    goto skip;
  end;


  while buffercountdown >= row_countdown do
  begin
    fill_length := row_countdown;
    mix_in_channels(fill_length);
    dec(buffercountdown, fill_length);
    if actual_row = patternheader.rows + 1 then goto_next_sequ;
    read_row;
    row_countdown := patternheader.spl_per_row;

    for track := 0 to 7 do (* Event-Infos applizieren *)
      if pattern_row[track].event then
      if pattern_row[track].smp > 0 then
      begin
        play_sample[track] := true;
        smp_poi[track] := sample_pointer[pattern_row[track].smp];

        smp_position[track].vor := 0;
        smp_position[track].nach := 0;

        smp_speed[track].vor := pattern_row[track].fixvor;
        smp_speed[track].nach := pattern_row[track].fixnach;

        smp_len[track] := smpinfo[pattern_row[track].smp].length;

        chan_volume[track] := pattern_row[track].vol;
        sample_loopstart[track]
          := smpinfo[pattern_row[track].smp].loopstart;
        if sample_loopstart[track] < smpinfo[pattern_row[track].smp].length
          then sample_loop[track] := true
        else
          sample_loop[track] := false;

      end
      else
        play_sample[track] := false;

  end;


  if buffercountdown > 0 then
  begin
    fill_length := buffercountdown;
    mix_in_channels(fill_length);
    dec(row_countdown, fill_length);
  end;


  skip:
(* Puffer gefuellt *)
  blockwrite(bufferfile, soundbuffer_pointer^, soundbuffer_size * 2);

end;
Wie man am Schluss sieht, bin ich noch in der Phase dass ich den Puffer nur als Datei ausgebe.
Das ist aber andererseits auch wichtig, weil man z.B. durch irgendwelche Fehler hervorgerufene
DC Offsets nicht hört, aber im Wave-Editor sehen kann.
"mix_in_channels" ist die Zentrale Routine welche eine ganze Zeile oder einen Bruchteil
davon in den Puffer addiert. Die ist im Moment noch in Pascal, und es funktioniert in Dosbox
auch schon ca. 10x so schnell wie der Song selbst spielt.
Habe auch schon getestet, alle Möglichen Puffergrößen funktionieren, seien es 512 Samples oder auch 16000.

Ich schätze ich bin in der ganzen Sache etwas umständlich rangegangen, möglicherweise.
Eine Optimierung könnte beispielsweise sein, dass ich eine Patternzeile aus den Patterndaten
nicht zuerst 1:1 einlese, also mit Nullen wenn kein Event, sondern dass ich quasi das Ausnullen
dieser Patternzeile weglasse, so dass ich keine Übergangsvariablen mehr brauche, die sich
den aktuellen Status eines Samples bzw. Tracks merken.
Wenn alles mal fertig ist und funktioniert werde ich wohl einiges rauskürzen, was mir im Moment
aber noch der besseren Übersicht dient.

Ich benutze übrigens einen 16 Bit Puffer. Dort hinein werden die Samples addiert und vorher mit der
Lautstärke (0-7) arithmetisch nach links geshiftet. Wenn alle Kanäle reingerechnet sind wird nochmal nach
einer festgelegten Lautstärkereduktion rechtsgeshiftet und dann auf -128 bis 127 geclippt, danach der Puffer
in einen 8 Bit Puffer überführt damit er vom Soundblaster ausgegeben werden kann. So ist jedenfalls
mein Plan und so hab ich es auch in einer alten Soundengine gemacht.
Man kann natürlich auch die Lautstärkereduktion so einstellen, dass das Clipping gar nicht greift.
Mir ist der 16 Bit Puffer und das Shifting nach links (statt nach rechts) nur wichtig, weil sonst ein Sample
mit einer Lautstärkeverminderung schon im Moment des Einmischens an Bittiefe einbußt und man sich
heftiges Rauschen einfängt, was gerade bei niedrigen Samplefrequenzen und ohne Interpolation nervig werden kann.

Wie machst du es denn bei dir, dass sich die Wellenformen sauber wiederholen?
Berechnest du das irgendwie mit modulo?


Jetzt kommen auf jeden Fall erstmal die zentralen Mischroutinen in Assembler.
Nicht besonders lang oder komplex, aber trotzdem werden die Register knapp.
Wäre nochmal hilfreich zu wissen, welche Tricks es da gibt und welche Register
man "missbrauchen" kann. Aber ich mach mich einfach mal an die Arbeit, optimieren
kann man dann noch.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

So, die Assembler-Mischroutine funktioniert jetzt endlich. Ist bedeutend schneller
als in Pascal-Code, ist aber eine ziemliche Bit-Kirmes geworden, mit dem ganzen rotieren...
Gibt es vielleicht eine Möglichkeit wie man direkt auf das obere WORD
eines 32 Bit Registers zugreifen kann?

Hier mal die Routine mit Sample-Schleife, mag etwas unbeholfen erscheinen, und ich würde
mich über Verbesserungshinweise freuen d.h. wie man sich vielleicht die ganze Rotiererei
oder auch das PUSH und POP von AX sparen kann:

Code: Alles auswählen

procedure mix_in_channel_sloop(length: word);assembler;
asm

  (* Variablen uebertragen bevor DS geaendert wird *)

  db 66h; mov bx, word ptr s_pos
  db 66h; mov dx, word ptr s_speed

  (* EBX enthaelt Sample Position *)
  (* EDX enthaelt Schrittweite *)


  mov ax, actual_sample_loopstart
  db 66h; shl ax, 16
  (* Schleifenbeginn wird im oberen WORD von EAX gehalten *)

  (* Lautstaerke Shift umrechnen und in oberen Teil von ECX schieben *)
  mov cl, 7
  mov ch, actual_chan_vol
  sub cl, ch
  db 66h; shl cx, 16
  (* Ich denke ich werde das im Format aendern, *)
  (* so dass diese Umrechnung nicht mehr gemacht werden muss *)

  mov cx, length

  les di, soundbuffer_pointer
  mov ax, mix_position
  shl ax, 1
  add di, ax
  (* es:[di] zeigt nun auf die richtige Mixposition *)

  (AX enthaelt Sample-Laenge *)
  mov ax, actual_smplen

  push ds
  lds si, smp_pointer


  @loopstart:

  (* aus Sample laden *)

  db 66h; ror bx, 16

  push ax
  mov al, ds:[si+bx]
  cbw

  (* Lautstaerke... SAR AX, 1 damit 8 Kanaele in 16 Bit passen *)
  db 66h; ror cx, 16
  sal ax, cl
  sar ax, 1
  db 66h; ror cx, 16

  (* zu 16 Bit Puffer addieren *)
  add es:[di], ax
  pop ax

  (* Sample Position mit Sample-Laenge vergleichen *)
  cmp bx, ax
  jb @1

  (* Diese Routine ist fuer Schleifen, also Leseposition (BX) *)
  (* an den Schleifenanfang setzen wenn BX >= AX *)
  (* das Nachkomma verbleibt hier unveraendert *)
  db 66h; ror ax, 16
  mov bx, ax
  db 66h; ror ax, 16

  @1:

  db 66h; ror bx, 16

  (* Pufferposition weiterzaehlen *)
  inc di
  inc di

  (* Sampleposition weiterzaehlen *)
  db 66h; add bx, dx

  dec cx
  jnz @loopstart

  pop ds

  (* Sample Position aus EBX in die Speichervariable uebergeben *)
  db 66h; mov word ptr s_pos, bx

end;
mov ax, 13h
int 10h

while vorne_frei do vor;
DOSferatu
DOS-Übermensch
Beiträge: 1220
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

zatzen hat geschrieben:Da ich eher so nebenbei programmiere, also nur alle paar Monate mal für einige Wochen Lust drauf habe,
und weil ich das jetzt so einschätze, stecke ich mir meine Ziele auch entsprechend anders. So würde ich
was ein Spiel angeht jetzt auch wieder mehr in Richtung simpleres Jump&Run gehen. Überhaupt einfach
mal etwas machen um ZVID und ZSTM einzusetzen.
Ja, ich würd auch gerne endlich mal mein Zeug einsetzen wollen. Aber für einen "Schnellschuß" (also ein Spiel, mal so eben in 'ner halben Woche hingemurkst) kann man die Sachen die ich da habe, nicht gebrauchen. Sie erlauben komplexe Dinge - aber dafür muß man sich dann auch leider drauf einlassen. Weder ARCADE01, GAMESYS2 noch ISM sind dafür geeignet - am ehesten noch ARCADE01 - da kann man auch fix mal was murksen...
zatzen hat geschrieben: [...]
Wenn ich einfach nur ein Spiel umsetzen wollte, sagen wir ein Adventure, und ich hätte die ganzen Grafiken
und ne gute Story - dann würde ich wahrscheinlich sowas wie AGS nehmen und mir das Programmieren sparen.
Aber ganz im Gegenteil, ich habe schon Spaß daran einen eigenen Musikplayer zu machen, und als der
jetzt vor ein paar Tagen endlich funktionierte hab ich mich richtig gefreut.
Ja, natürlich macht es mir auch wesentlich mehr Spaß, 100% selbstgemachtes Zeug zu verwenden. Die ganze Programmierung ist sowieso der meiste Spaß daran. Irgendwelches vorgefertigtes Zeug... das ist was für USER...

Ich habe jetzt mal nicht Dein Listing analysiert/kommentiert.
zatzen hat geschrieben: [...]
Wie machst du es denn bei dir, dass sich die Wellenformen sauber wiederholen?
Berechnest du das irgendwie mit modulo?
Nein, ich benutze Festkomma. Hatte ich ja letztens schon erklärt.
Ich addiere 32bit-Additionswerte zu einem 32bit-Register und die oberen 16bit dann AND $1F (weil 32 Bytes Welle, also immer obere Bits ausmaskieren). Dadurch, daß die unteren 16 Bits dann nichts vom "Wraparound" mitkriegen, passiert der saubere Übergang von ganz allein.
zatzen hat geschrieben:So, die Assembler-Mischroutine funktioniert jetzt endlich. Ist bedeutend schneller
als in Pascal-Code, ist aber eine ziemliche Bit-Kirmes geworden, mit dem ganzen rotieren...
Gibt es vielleicht eine Möglichkeit wie man direkt auf das obere WORD
eines 32 Bit Registers zugreifen kann?
Leider nicht. Deshalb benutze ich ja den selbstentwickelten "Trick", den ich da letztens erwähnt habe:
Ich vertausche einfach High- und Lowword, damit das Lowword als Indexregister benutzt werden kann.
Zu diesem 16/16-vertauschen Register addiere ich einen Wert, der ebenfalls 16/16 vertauscht ist. Natürlich ist damit der Übertrag zwischen Bit 15 und Bit 16 futsch, denn die sind ja nun Bit 31 und Bit 0. Glücklicherweise wirft ja jede Addition nachher den Übertrag ins CARRY - und man kann ja auch mit Carry addieren. Somit sind es 2 Additionen.

Achja - was passiert, wenn das untere aufs obere WORD überträgt? Das soll ja eigentlich nicht passieren, denn Bit 15 ist ja eigentlich das gedachte Bit 31 und Bit 16 ist das gedachte Bit 0 (des "unteren" Words). Naja - gar nichts - weil dieser Fall nicht auftritt. Es gibt IMMER noch einen "Bereinigungs"-Befehl für das untere Word danach, der es "in seine Schranken weist". z.B. für die Welle ist es AND Register,$1F (weil die Welle 32 Bytes lang ist).

So sieht das Ganze in Assembler aus:

Code: Alles auswählen

add EBP,addierwert
adc BP,0
and BP,$1F
Der zweite Befehl, der "Null addiert", addiert in Wahrheit 0 oder 1 - je nachdem, ob der erste einen Übertrag hatte. Das erzeugt dann den Übertrag zwischen gedachtem Bit 15 und 16.

Der Addierwert, der ja aus der Abspielfrequenz für den Puffer, und die Notenfrequenz ermittelt wird und ein 32-Bit-Wert ist (Festkomma 16:16) wird natürlich ebenfalls "getauscht", nachdem er ermittelt wurde (ROL Addierwert,16 oder ROR Addierwert,16 - es ist egal, beide tauschen oberes und unteres Word aus).

Ja, ich weiß, das wirkt creepy - aber es funktioniert. Ich hatte nämlich ebenfalls damit gehadert, daß es keine gescheite Möglichkeit gibt, NUR die oberen 16bit eines 32bit (Exx) Registers zu benutzen. Und da hatte ich dann diesen Einfall.
Ich weiß nicht, ob schon sonst jemand diese Idee hatte. Ich schau mir eigentlich keine größeren ASM-Listings anderer Leute an.

Ich hab ja auch noch andere Einfälle, um 16-Bit-Register zu erhöhen und gleichzeitig zu testen/springen, wenn sie einen bestimmten Grenzwert erreichen - OHNE CMP dafür zu brauchen:
Ich benutze einfach das entsprechend erweiterte 32bit-Register und zu Anfang schiebe ich die gewünschte Anzahl Durchläufe-1 in die oberen 16 Bit. Dann setze ich die unteren 16 Bit auf den (beliebigen) Startwert.
Wie das geht? Naja, ich erhöhe dann die unteren 16 Bit und vermindere gleichzeitig die oberen 16 Bit (mit dem gleichen Befehl):
add EAX,$FFFF0001

Eine Addition mit $FFFF ist wie eine Subtraktion mit 1. Nur daß quasi jetzt JEDESMAL ein Übertrag erfolgt. D.h. das Carry ist IMMER gesetzt... außer....? Genau! Außer wenn die oberen 16 Bit=0 sind! Das ist dann quasi der "Underrun". Und so kann ich 1 bis 65535 Durchläufe machen. Und die unteren 16bit müssen nicht nur um 1 erhöht werden - hier ist jeder Wert möglich, auch ein "krummer" wie 5 oder so, wenn man das braucht. Einzig wichtig ist nur, daß die unteren 16 Bit nicht "überlaufen" - aber das ist ja genau das, was man sowieso mit dem "Unterlauf-Test" erreichen will.

So kann man dann entweder bei Unterlauf aus der Schleife herausspringen (meine Schleifen haben ja mehrere Abfragen nach Endwerten) - mit JNC @outofloop oder, solange der Unterlauf noch nicht erreicht ist, zum Schleifenanfang -
mit JC @loopstart.

Eine andere Möglichkeit wäre, die oberen 16 Bit auf minus Anzahl Durchläufe zu setzen (also für 5 Durchläufe dann -5, also $FFFB) und dann immer add $00010001 zu rechnen, dann ist der Überlauf bei gesetztem Carry. (Das war meine erste Idee dazu.)
Nachteil ist aber, daß man da vorher erst den Wert negieren muß und ob man add $00010001 oder add $FFFF0001 rechnet, macht performancetechnisch keinen Unterschied. Man muß nur daran denken, daß sich das Verhalten des Carry dann umkehrt. Somit bin ich mit der $FFFF0001 Methode ganz zufrieden.

Naja, und so weiter. Wenn man eine Weile mit ASM und den praktischen Flags rumspielt, kommt man auf eine Menge nützlicher Ideen - Ideen, die KEINE mir bekannte Hochsprache supportet!

Ein Monochromdisplay Bitweise mit Pixeln bestücken und danach das Pixelbit und bei Bedarf die Byte-Position erhöhen?
-> rol AL,1; adc BX,0;
Das soll mir ne Hochsprache mal nachmachen! Das Pixelbit ist beim "Überlauf" wieder unten, BX erhöht sich nur, wenn es einen Überlauf gab. Alles ohne Sprünge!

und, btw: Habe das erst kürzlich mal bemerkt:
ich verwende die shl register,1 Befehle nicht mehr, sondern benutze stattdessen add register,register.
Klingt erstmal blöd, weil ja jeder, der *2 rechnet, spontan erstmal Shiften würde - ABER:
Ich erhalte das gleiche Ergebnis, auch das gleiche Carry-Flag, (und es werden sogar mehr nützliche Flags gesetzt als beim SHL) und vor allem: Es braucht weniger Taktzyklen! - Klingt komisch - ist aber so.

OK, genug davon - vielleicht hilft Dir das ja.

Ich würde ja den Source zu ISM posten... aber das sind knapp 111 kB Sourcecode (ca. 113500 Byte, compiliert sind es derzeit 13650 Bytes), und da ist die dynamische Speicherverwaltung für die 4bit-gespeicherten Samples noch nicht mal mit dabei. Und ich schreibe nicht zeilenweise, sondern Befehle nebeneinander, mit Semikolon getrennt. Und natürlich außerdem in diesem "Pascal-Assembler", der nur max. 286er kann, also ist alles "386er-mäßige" in Bytecode... Und es ist eher recht spärlich kommentiert. Für jemanden, der das nicht selbst zusammengebaut hat, liest sich das bestimmt wie Sanskrit. Ich bin mir für keinen miesen Trick zu schade, wenn er mehr Performance rausholt. Und ja, selbstmodifizierender Code gehört da (leider) auch dazu - ... tja, was soll man machen? Manchmal gehen einem einfach die Register aus...

Ich kann das einfach nicht abhaben, wenn irgendwas nicht so performt, wie es könnte - oder sollte. Das erklärt wohl auch, wieso ich nicht gerade ein glühender Verehrer von MS-Windows bin...

Achja, einen Link will ich Dir nicht vorenthalten. Guck mal, was ich gefunden habe:
http://www.frischmann-marzipan.com/Marz ... :3730.html
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Der Hase Jockel :)
Wie die bloß auf den Namen gekommen sind...
Vielleicht arbeitet bei denen jemand der mein Liedchen gehört hat.


Aber gute Idee das mit dem verdrehen der 32 Bit Register. Habe ich ja im Prinzip
auch so gemacht, bloß bin ich nicht auf die Idee gekommen auch so zu addieren.
Damit würde ich mir das Rotieren während der Schleife sparen und müsste es nur
ausserhalb machen.
Und wenn ich es richtig sehe ist die "Gefahr" nur da, wenn das WORD, das den
Vorkomma-Anteil enthält, einen Überlauf hätte. Bei dir ist das mit dem AND 31
sowieso verhindert, bei mir wird es nicht auftreten, weil ein Sample Pascal-typisch
nur 65528 Byte belegen kann und es bis zum Überlauf noch 8 Byte wären, diese aber
wahrscheinlich nie überschritten werden weil eine Schrittweite von 8 schon höchst
unwahrscheinlich ist, vor allem bei einer Mixfrequenz von 16kHz. Da müsste schon
jemand mit dekadent hohen Samplerates für Samples kommen (z.B. 44.1 kHz)
und dann noch mehr als eine Oktave höher gehen als die Grundfrequenz.

Bleibt aber noch die Rotiererei mit CX und AX.
Und ich brauche glaube ich drei weitere Register wenn ich das vermeiden will und
auch auf den Stack verzichte. Du benutzt z.B. BP. Wären SS und SP auch zu haben
wenn man diese Register vorher im Speicher sichert?
Uff, das erscheint mir gerade sehr kompliziert, DS wird ja auch verändert...
Naja ich versuch mal mich selber schlau zu machen.

Was Source Code angeht, ich bin ja froh dass ich durch meinen eigenen durchblicke.
Deshalb schreibe ich ja auch so betont übersichtlich mit Einrücken und Zeilen frei
lassen. Das macht auch irgendwie Spass. Aber ich stecke da einfach nicht so dauerhaft
drin und wenn ich eine paar Wochen oder Monate nicht programmiere würde ich da gar
keinen Zugang mehr finden ohne übersichtliche Formatierung und ausführliche Kommentare.
Aber so geht's und es funktioniert sogar besser als früher, als ich noch viel mehr programmierte.

Der Player ist bald fertig, habe heute noch einen Fehler gefixt, jetzt gehts eigentlich nur
noch darum einen netten Song zu haben den man sich zum einen gut anhören kann und
der zum anderen auch ein bisschen die Vorzüge des Formats demonstriert. Wenn man
da überhaupt Vorzüge sehen will. Klar, die Patternkompression ist gut, macht sich
aber nur bezahlt wenn man wenig Sampledaten hat oder aber sehr viele Patterns.
Ansonsten ist das ganze einfach nur ein sehr abgespeckter 8-Kanal-MOD-Player
der keine Effekte kann, absolut nicht zeitgemäß - sowas programmiert man heute
eher auf einem raspberry pi aber mit vollem Effektumfang - aber ich bin nur ein
kleiner Nebenprogrammierer der ab und an Spass an der Sache hat, und jetzt hab
ich eben meinen eigenen Player, was ich immer schonmal haben wollte.
mov ax, 13h
int 10h

while vorne_frei do vor;
Antworten