PDA

View Full Version : Byte-by-byte allocated data manipulation



WILL
09-12-2007, 09:02 PM
I have a scenario where I am allocating a chunk of data for audio playback. The audio I'm loading from a MOD(XM/FastTracker2 to be specific) sample data. The trick however is that because the sample data is delta encoded for compression reasons, I now have to go byte-by-byte or word-by-word (for 16-bit audio) through the whole thing and run it through a simple decryption algo.

Here is the algo in semi-Pascal:
var
old, new: ShortInt;

old := 0;
for i := 0 to data_len do
begin
new := sample[i] + old;
sample[i] := new;
old := new;
end;

Now how would I best access my audio data considering that I originally allocate and dump it like so...

{Sample Data}
for j := NumberOfSamples - XmInstrument.NumOfSamples to NumberOfSamples - 1 do
begin
GetMem(Samples[j].SampleData, Samples[j].SampleLength);

BlockRead(FileStream, Samples[j].SampleData^, Samples[j].SampleLength);
end;

I want to maintain my SampleData pointer as it is so that I can then later give this to whatever audio buffer loading function for OpenAL, DSound, etc... Beyond that I can use whatever additional structures I might need.


EDIT: I guess my original question should have been how can I access a specific piece of the data after allocating it via GetMem(). :)

EricLang
09-12-2007, 11:39 PM
I am not sure if I understand the question 100% but if I'm right you just want to access single bytes from a piece of memory.
There are several good ways of which I show two here to get the idea.

var
DataPtr: Pointer;
BytePtr: ^Byte;
DataSize: Integer;
i: Integer;
B: Byte;

type
TByteArray = array[0..100000000] of Byte;
PByteArray = ^TByteArray;
var
BA: PByteArray;
begin
DataSize := 100;
GetMem(DataPtr, DataSize);
FillChar(DataPtr^, DataSize, 1);

// method 1: typed pointer increment
BytePtr := @DataPtr^;
for i := 0 to DataSize - 1 do
begin
B := BytePtr^;
Inc(BytePtr); // type pointer increment: address of BytePtr moves 1 byte
end;

// Method 2: array typecast
BA := @DataPtr^;
for i := 0 to DataSize - 1 do
begin
BytePtr := @BA^[i]; // make byteptr point to the right byte
B := BA^[i]; // fill byte with value
end;

FreeMem(DataPtr);
end;

WILL
10-12-2007, 01:37 AM
Yup, you've basically got it. :)

I also want to manipulate it then feed it back. I imagine I'd be able to assign it back to the Byte/Word pointer in the same manner?

WILL
10-12-2007, 05:37 AM
Actually the strangest thing is that I have to do also convert it from a signed value to an unsigned one. So I guess I'm looking at making all of these...

var
AudioBuffer_8bit_Signed : ^ShortInt;
AudioBuffer_16bit_Signed : ^SmallInt;
AudioBuffer_8bit_Unsigned : ^Byte;
AudioBuffer_16bit_Unsigned : ^Word;


...point the ones that I need (8 or 16 bit pairs) and then pass the data through them to convert it.

That is to say with the issue of how the data will be altered aside. ;) (see my 'Sound' forum thread (http://www.pascalgamedevelopment.com/viewtopic.php?t=5106) if anyone wants to help me with that one.)


So anyhow I'd imagine that I'm on the right track?

Mirage
10-12-2007, 07:02 AM
I recommend to use dynamic arrays.;)


SetLength(Samples[j].SampleData, Samples[j].SampleLength); // Instead of GetMem
if Samples[j].SampleLength > 0 then BlockRead(FileStream, Samples[j].SampleData[0], Samples[j].SampleLength * SizeOf(Samples[j].SampleData[0]));

where Samples[j].SampleData is declared like this:


SampleData: array of Byte; // or whatever you want instead of byte


Of course you can manipulate with the array data as you wish.

waran
11-12-2007, 06:31 AM
If you are using Freepascal then you can use any pointer as array (C-Style).
"someWordPtr[20] := 0" -> would access element #19.

If not then you can only cast the pointer to an array or do the
pointer-arithmetics by yourself: "inc(integer(workPtr), x*sizeof(someType))"

Note that range checking doesn't work, of course.

imcold
12-12-2007, 07:00 AM
If you are using Freepascal then you can use any pointer as array (C-Style).
"someWordPtr[20] := 0" -> would access element #19.
SomeWordPtr[20] would access element no. 21.

jdarling
12-12-2007, 01:54 PM
If you are using Freepascal then you can use any pointer as array (C-Style).
"someWordPtr[20] := 0" -> would access element #19.
SomeWordPtr[20] would access element no. 21.
That depends on what settings you have turned on in FPC.

WILL, take a look at PChar's and PWord's. They are the fastest methods for direct memory addressing. Same as in C/C++ you can quickly inc and dec them as well as De-reference them for direct access. You can also do things such as (p+N)^ to access the Nth item (just remember that you will always be looking at N+1).

Final note, PChar's and PWord's take less overhead then dynamic arrays but they do require that you alloc the block up front. Dynamic arrays take longer to access if your talking about Bytes or Words in comparison, but they do offer an easy way of resizing your structure if necessary.

waran
12-12-2007, 04:22 PM
@imcold
yeah, of course. Mixed it up - sry!

@jdarling
Delphis dynamic arrays aren't slower than any pointer since they
use the very same technique. However they need a bit more space
because they store also their lengths (you would do anyway when
using a pointer, I guess).

imcold
13-12-2007, 10:52 AM
I agree with waran, the speed should be the same and I wouldn't wonder if the generated code would be the same, too. I'm not aware of any fpc switch that would make SomeWordPtr[20] point to something other than to elem. no. 21 (providing it's pointing at the beginning of some word array). Dynamic arrays need to be allocated before accessing just like pointers; the allocated memory can be resized with ReAllocMem easily.

jdarling
13-12-2007, 03:58 PM
All I can say is that in testing I've noticed that Dynamic arrays are much slower then pointer characters.
procedure RunArray(A : Array of Byte);
var
I : Integer;
A : Array of byte;
begin
for I := 0 to Length(A) -1 do
DoSomething(A[I]);
end;

Compared to:procedure RunPChar(PC : PChar);
begin
while PC^ <> #0 do
begin
DoSomething(PC^);
inc(PC);
end;
end;

I'll grant that the Length calculation takes time, but in the end it doesn't explain the difference. Of course if you really want to get apples to apples then you need something closer to the following (where the array is still slower due to the need to calculate the offset each time):

procedure RunArray(A : Array of Byte; l : Integer);
var
I : Integer;
A : Array of byte;
begin
for I := 0 to l-1 do
DoSomething(A[I]);
end;

Compared to:procedure RunPChar(PC : PChar; l : Integer);
var
i : Integer;
begin
for I := 0 to l-1 do
begin
DoSomething(PC^);
inc(PC);
end;
end;

The reason for the speed difference (as I sated just before the 2nd code blocks) is that any time you access a dynamic (or static) array the offsets need to be calculated. With a PChar the inc takes care of movement and no calculation is needed to determine offset.

One way around this would be to keep a pointer into the array and increment that pointer. But then, your back to using pointer math and why not just use PChar's to start with.

waran
13-12-2007, 04:17 PM
The correct pendant to [...]
Edit: Deleted my code. Never mind :)

As you stated: You need to get a new adress every cycle.
Thats not a problem of the delphi dynamic array!
If you would do "somePchar[i]" every iteration (either in C or FPC)
it would be equally slow.

However: PChars are slow, because you need to calculate lengths
very often (for about every operation), its hard to access specific
elements (if you are using delphi) and: If you merely forget one 0 then
your array is busted and you program will go nuts.

But as far as normal arrays (not strings) are concerned:
- The point with the specific element stays.

If you need top-performance you can also iterate through a dynamic
array via pointer. Thats no drawback - while enjoying all features
(like easy resizing; it needs at least some complicated reallocation for
pure pointer-"arrays").


So to bottomline it:
There is no speed difference in accessing pointers or dynamic arrays.
someArray[i] is the very same as (somePointer+i)^

jdarling
13-12-2007, 04:36 PM
Thats why I added in the 2nd block's of code. Notice I said I wasn't comparing apples to apples in the first.

If your using a working pointer then your back to pointer math and are basically working with PChars (when dealing with bytes). No sense in actually creating an array and having the extra overhead at that point, instead just allocate the PChar and go. Granted your only talking a SizeOf(SizeInt) difference, but if you have a few 1000 instances that SizeOf adds up quickly.

When I created my generic parser and lexer libraries I spent a lot of time performing tests to see what the fastest method to iterate strings was, PChar always won in size and speed comparisons.

The other nice thing is that AnsiString to PChar casting is lossless where Array to AnsiString or AnsiString to Array causes a mem copy.

I will say that arrays win if you are doing something as follows:procedure LoadDataFile(FromFile : AnsiString);
var
fs : TFileStream;
iCount : Integer;
begin
fs := TFileStream.Create(FromFile, fmOpenRead);
try
iCount := fs.Size div SizeOf(TMyRecordType);
SetLength(Data, iCount);
fs.Read(Data[0], iCount * SizeOf(TMyRecordType);
finally
fs.Free;
end;
end;
(that code isn't tested at all LOL)

This is mainly due to the fact that using direct blocks of memory (PChar or pointer) you will have to perform a copy with block reads instead of a single direct read from the origin. Behind the scenes, FPC (and Delphi for that matter) handle these block reads for you in the scenario above and use pointer math to move through the target.

[Edit] PS: Your retort proves my point, any time you can move to pointer access over indexed access its faster. You can go back through everything I posted and change it to Pointer instead of PChar and its the same thing. I'm speaking against indexed access when compared to pointer access. In the end, I believe, we are both saying pointer access is faster than indexed access.

waran
13-12-2007, 04:43 PM
lets take it to the peak:


var
a: array of integer;
i: integer;
begin
setlength(a, 20);
for i := a[0] to a[19] do doSomething(i);
end.


... we calculate 1 offset only.
Ok. Now tried it ... doesn't work, sadly :(

Yes, we are indeed saying the same. The main thing I want to state
is that a[i] and (a+i)^ is the same.
Of course calculating 20 offsets is slower than calculating 1 and
then increment the pointer.

VilleK
13-12-2007, 04:49 PM
procedure RunArray(A : Array of Byte);
var
I : Integer;
A : Array of byte;
begin
for I := 0 to Length(A) -1 do
DoSomething(A[I]);
end;


Two problems with above code:

1. A declared twice so it won't compile in Delphi
2. The parameter is passed by value, which means it is copied every time.

This version which should be as fast as pchar:

procedure RunArray(const A : Array of Byte);
var
I : Integer;
begin
for I := 0 to Length(A) -1 do
DoSomething(A[I]);
end;

waran
13-12-2007, 05:51 PM
@ VilleK, its slightly slower.


uses windows;

procedure Arr(var a: array of integer);
var
wrkPointer: ^integer;
i: integer;
begin
wrkPointer := a;
for i:=0 to high(a) do
begin
wrkPointer^ := wrkPointer^ div 3;
inc(wrkPointer);
end;
end;

procedure Arr2(var a: array of integer);
var
i: integer;
begin
for i:=0 to high(a) do a[i] := a[i] div 3;
end;

var
a: array of integer;
i: integer;
t1,t2,fq: int64;
begin
setlength(a, 100000);
for i:=0 to high(a) do a[i] := random($FFFFFFFF);

QueryPerformanceFrequency(fq);
QueryPerformanceCounter(t1);
Arr(a);
//Arr2(a);
QueryPerformanceCounter(t2);

writeln(((t2-t1)/fq):8:8);
end.


Results:
0.00022s (for Arr)
0.00026s (for Arr2)

I don't know if this plus in performance really justifies the means.

Mirage
13-12-2007, 06:57 PM
Sadly, Delphi can't optimize sequential access to an array (doesn't matter dynamic one or not), so pointer increment works faster in that case. But random/indirect access performance is equal.
Main benefit of dynamic arrays is safety. With pointer math you can easily violate array bounds. And this may emerge as a weird, difficult to catch bug.
With dynamic arrays there is range checking in debug builds which actually saves a lot of debugging time.
So pointer math is sensible only in time-critical parts of code and as an optimization over dynamic array-based code.

UPD:
waran, the line

wrkPointer &#58;= @a;
is incorrect. There should be:

wrkPointer &#58;= @a&#91;0&#93;;

But performance will be the same of course.:)

waran
13-12-2007, 08:44 PM
Or just "a" without @.
Corrected it.

WILL
14-12-2007, 02:40 AM
Ok fair enough guys. :) But can anyone confirm my follow-up question? :P


I also want to manipulate it then feed it back. I imagine I'd be able to assign it back to the Byte/Word pointer in the same manner?

What I mean is; so far I understand that I can read the value of a sample in an allocation of memory using the byte and word pointers, but would I be able to then assign values back to the allocated memory using these same pointers?

var
Bob: byte;
Chuck: ^byte;

Chuck := @MemoryAddress^; // Get Address I need
Bob := Chuck^; // Read data I need
inc(Bob); // Mess with it...
Chuck^ := Bob; // Stick it back in there how I need it!

Further; if this will do the trick, are there any issues that would help to know?

Also can I easily extract the data and the inser it in as a diferent format? ie. read a signed byte/word value and then feed back an unsigned byte/word value?


I realize that these questions might seem a bit 'simple', but it's been a while and I'm way out of practice. Thanks! ;)

User137
14-12-2007, 04:09 AM
Try keep variable types the same on source and destination. Extra type conversions takes time and makes errors possible. Especially if you have
var
source: smallint;
dest: word;
begin
source:=-1;
dest:=source; // Now dest would be something over 30000 i guess
// or vice versa, unsigned to signed can result in number go negative
// or larger type to small type can make the value looping like with mod
end;
You know what i mean...

And your code seems valid.

imcold
14-12-2007, 05:14 AM
What I mean is; so far I understand that I can read the value of a sample in an allocation of memory using the byte and word pointers, but would I be able to then assign values back to the allocated memory using these same pointers?
Yes.
Chuck := @MemoryAddress^; // Get Address I need

If MemoryAddress is a pointer, Chuck := MemoryAddress; is ok too.


Also can I easily extract the data and the inser it in as a diferent format? ie. read a signed byte/word value and then feed back an unsigned byte/word value?
The format for signed and unsigned values depends on how you treat the array:
var
Bob: shortint;
Chuck: pshortint;

Chuck := @MemoryAddress^; // access same memory
Bob := Chuck^; // Read signed data I need
inc(Bob); // Mess with it...
Chuck^ := Bob; // Stick it back in there how I need it!
The values that are over 127 will be negative now.
As for the OpenAL issue... try to feed it with signed data and see what happens, if it treats the data as signed values internally, it should work.

jdarling
14-12-2007, 01:52 PM
This is off of the top of my head and without an IDE to test in, but I think I got it close. This is based off of the original source WILL posted.// This routine doesn't allocate or check memory boundries, thats left up to you
procedure DecodeBlock(SrcPtr, DstPtr : Pointer; Length : SizeInt; InitialVal : ShortInt = 0);
var
Start : Pointer;
old,
worker : ShortInt;
begin
old := InitialVal;
Start := SrcPtr;
while ((SrcPtr-Start)<Length) do
begin
// First get a value from the source pointer
worker := PShortInt(SrcPtr)^;
// Now perform your math on the worker
worker := worker + old;
// Reset the old value for the next itteration
old := worker;
// Write out the new value
DstPtr^ := worker;
// Move forward
inc(SrcPtr, SizeOf(worker));
inc(DstPtr, SizeOf(Worker));
end;
end;

procedure LoadSamples(Instrument : TInstrumentType; NumberOfSamples : SizeInt; FileStream : TFileStream);
var
i : Integer;
begin
for i := NumberOfSamples - Instrument.NumOfSamples to NumberOfSamples - 1 do
begin
// Get a worker block the size of the sample data
// NOTE : DON'T FORGET TO FREE IT LATER WHEN YOU FREE INSTRUMENT
GetMem(Samples[i].SampleData, Samples[i].SampleLength);
// Load the file data from the file
FileStream.ReadBuffer(Samples[i].SampleData^, Samples[i].SampleLength);
// Decode the memory
DecodeBlock(Samples[i].SampleData^, Samples[i].SampleData^, Samples[i].SampleLength);
end;
end;

WILL
14-12-2007, 09:10 PM
Excellent! :) Yes, thats essentially what I need to do.

(Jer: You know of delta encoding I see. ;))

The issue with OpenAL is that it doesn't accept signed sample data. So I have to convert it all from the allocation that I have that will either be 16-bit or 8-bit. After I have a converted/decoded allocation of memory THAT is my sample so I won't need to manipulate it further. Just feed it, or part of it, into an AL sound buffer for mixing/playback.

The problem is 2-fold. (or 3 if you consider the delta encoding) So I don't want to get into the second part in this thread much as I just want to figure out the manipulation of memory for now.

In THIS THREAD (http://www.pascalgamedevelopment.com/viewtopic.php?t=5106) I have posted the question of how I take audio sample data that is signed and properly convert it to unsigned. The data will obviously remain the same byte-size, but will merely go from signed to unsigned.

So I'd be taking a ShortInt(8-bit signed) and turning it into a Byte(8-bit unsigned).

...and then...

taking a SmallInt(16-bit signed) and turning it into a Word(16-bit unsigned).

WILL
14-12-2007, 09:15 PM
Try keep variable types the same on source and destination. Extra type conversions takes time and makes errors possible. Especially if you have
var
source: smallint;
dest: word;
begin
source:=-1;
dest:=source; // Now dest would be something over 30000 i guess
// or vice versa, unsigned to signed can result in number go negative
// or larger type to small type can make the value looping like with mod
end;
You know what i mean...

And your code seems valid.

Well looking at the problem that way, I can see how I might go wrong.

I however would instead take the signed value into a local variable move that to another variable of the required sample data type and then copy that into the allocated data pointer. :)

So from pointer to different typecast pointer OR from variable to different type cast pointer I can see how it might mess up my values, BUT from variable to different typecast variable it shouldn't give me issues, right?

User137
14-12-2007, 09:55 PM
Looks to me it doesn't matter if you typecast or use pointer, both do the same
procedure TForm1.FormCreate(Sender: TObject);
var b: byte;
s: shortint; ps: PShortint;
begin
b:=230;
s:=b;
memo1.Lines.Add(inttostr(s)); // typecasted
ps:=@b;
memo1.Lines.Add(inttostr(ps^)); // pointer
end;
This results the memo1 to have 2 numbers which are in this case -26 and still same if i change b value to any other.

WILL
15-12-2007, 03:41 AM
I understand what you are saying, BUT...

I still have to convert data that is in 8 or 16 bit samples that have signed typecasting and convert it to unsigned samples. :)

This is now getting into the issue of how I calculate/convert the new sample data to fit into the new format than how I would actually write it into the allocated memory. Though... the it is where the two are somewhat tied.

Anyhow I think I know how to read/write to the memory, I just have to tackle the conversion issue now. :)

WILL
25-12-2007, 04:07 AM
Ran into a problem it seems...

I for some strange and unknown reason keep getting SEGSEGV error on the following indicated line below. Now my first assumption is that I'm trying to access a piece of memory that is not allocated. But my code should restrict this. I know it's this block of code because when I remove it, not a complaint whatsoever.

So... where am I making my mistake?

{Decode Sample Data}
// Decode Audio data!
for i := 0 to NumberOfSamples - 1 do
begin
if (Samples[i].is16Bit) then
begin
AudioBuffer_16bit_Signed := @Samples[i].SampleData;
AudioBuffer_16bit_Unsigned := @Samples[i].SampleData;

for j := 0 to Samples[i].SampleLength div 2 - 1 do
begin
SignedWordBuffer := AudioBuffer_16bit_Signed^; // << This is where I get my error!
WordBuffer := SignedWordBuffer + 32768;
AudioBuffer_16bit_Unsigned^ := WordBuffer;

if (j < Samples[i].SampleLength div 2 - 1) then
begin
inc(AudioBuffer_16bit_Signed, 2);
inc(AudioBuffer_16bit_Unsigned, 2);
end;
end;
end
else // not Samples[j].is16Bit // is 8-bit!
begin
AudioBuffer_8bit_Signed := @Samples[i].SampleData;
AudioBuffer_8bit_Unsigned := @Samples[i].SampleData;

for j := 0 to Samples[i].SampleLength - 1 do
begin
SignedByteBuffer := AudioBuffer_8bit_Signed^;
ByteBuffer := SignedByteBuffer + 128;
AudioBuffer_8bit_Unsigned^ := ByteBuffer;

if (j = 113570) then
WordBuffer := ByteBuffer;
if (j < Samples[i].SampleLength - 1) then
begin
inc(AudioBuffer_8bit_Signed, 1);
inc(AudioBuffer_8bit_Unsigned, 1);
end;
end;
end;
end;

WILL
25-12-2007, 04:09 AM
Oh and to be clear 'Samples[i].SampleLength' is the length of data in bytes.

Mirage
25-12-2007, 05:38 AM
inc(AudioBuffer_16bit_Signed, 2);

if the type of AudioBuffer_16bit_Signed is something like ^Shortint the line above is incorrect. Should be just

inc(AudioBuffer_16bit_Signed);

Because when you increment a pointer its type's size is automatically taken in accunt by the compiler.

Therefore I prefer dynamic arrays.;)

WILL
25-12-2007, 07:31 AM
Tried with your inc(..blah...); correction. Same issue. Funny thing is that I get the error well before the end of what the data should be.

Also the funniest thing... when I try to comment out all the code inside the block meant for 16-bit data I get the same SIVSEGV error and it also asks to locate 'astrings.inc'. Which just blows my mind. :? Why do I need to locate such a file and if I need it why on earth is it not there in the first place???

I'm getting the feeling that FPC is just tripping on me here... can anyone else see an error on my part? I can upload/post the whole code if you need...?

User137
25-12-2007, 11:52 AM
Do you need this block?
if (j < Samples[i].SampleLength div 2 - 1) then
It isn't needed in my opinion and actually should make buffers reading samples partially overlapped.

Other than that, what happens before the big loop? It's also important to know how pointers are tied to or which are direct parameters.

WILL
25-12-2007, 06:47 PM
I just realized that you were indicating the conditional for weither or not we are at the end of our segment of data. Imagine if I tried to increment the address one more time... I'd get an error for trying to access memory that was not allocated. :P So... all it does is restrict the pointer from incrementing on it's last iteration through the data.

As for what I've done with this data before...

{Sample Data}
for j := NumberOfSamples - XmInstrument.NumOfSamples to NumberOfSamples - 1 do
begin
GetMem(Samples[j].SampleData, Samples[j].SampleLength);

BlockRead(FileStream, Samples[j].SampleData^, Samples[j].SampleLength);
end;

This is the block of code that allocates the memory for each music sample/instrument and copies it. Samples[j].SampleLength is the length in bytes of the sample. Samples[j].SampleData is the Pointer that I use to address the data.

You may ask why the for loop is so odd looking. It has to do with the FastTracker2/XM format and how it's stored. (Oh how I'm missing the ease and simplicity of S3M. :?)

If this really gives you guys no clue I can post the entire project up for download and trials on your own systems. It'll be all a part of an opensource api down the road anyhow. :)

User137
25-12-2007, 08:27 PM
Oh my mistake, didn't notice these:

AudioBuffer_16bit_Signed := @Samples[i].SampleData;
AudioBuffer_16bit_Unsigned := @Samples[i].SampleData;

So ignore my comment about overlapping, but instead noticed a new thing.

So Samples[i].SampleData is pointer already. In that case @Samples[i].SampleData means pointer to pointer... Try instead
AudioBuffer_16bit_Signed := Samples[i].SampleData;
AudioBuffer_16bit_Unsigned := Samples[i].SampleData;

or

AudioBuffer_16bit_Signed := @Samples[i].SampleData^;
AudioBuffer_16bit_Unsigned := @Samples[i].SampleData^;

WILL
26-12-2007, 12:39 AM
Whoa... that totally threw me off kilter... :o The memory error went away. :lol:

So if thats how you properly assign a pointer to another pointer, what was I actually doing???

It's obviously either been way too long since I studied pointers and pointer operations OR I'm just not too familiar with how FPC does it.

Setharian
26-12-2007, 06:37 AM
You assigned address of the pointer variable, not the pointer itself. The reason you got SIGSEGV not immediately but only after some loops is that AudioBuffer16_ variables pointed to some place on the stack and it kept working until it reached end of the allocated stack space.

WILL
28-12-2007, 03:30 AM
Well that makes perfect sense then... :)

Well I'm past this problem and back to the decoding issue again. :P

Big THANKS guys! :thumbup: