lots of stuff

This commit is contained in:
Adam D. Ruppe 2020-06-15 10:46:51 -04:00
parent 3be163c44e
commit ae17d5a497
10 changed files with 4377 additions and 998 deletions

View File

@ -3,7 +3,7 @@
"targetType": "library",
"importPaths": ["."],
"sourceFiles": ["package.d"],
"description": "Subpackage collection for web, database, terminal ui, gui, scripting, and more with a commitment to long-term compatibility and stability.",
"description": "Subpackage collection for web, database, terminal ui, gui, scripting, and more with a commitment to long-term compatibility and stability. Use individual subpackages instead of the overall package to avoid bringing in things you don't need/want!",
"authors": ["Adam D. Ruppe"],
"license":"BSL-1.0",
"dependencies": {
@ -27,6 +27,7 @@
":terminal": "*",
":ttf": "*",
":color_base": "*",
"svg":"*",
":database_base": "*"
},
"subPackages": [

View File

@ -91,6 +91,11 @@
XInput is only supported on newer operating systems (Vista I think),
so I'm going to dynamically load it all and fallback on the old one if
it fails.
Other fancy joysticks work low level on linux at least but the high level api reduces them to boredom but like
hey the events are still there and it still basically works, you'd just have to give a custom mapping.
*/
module arsd.joystick;
@ -883,7 +888,7 @@ version(linux) {
printf("\n");
while(true) {
int r = read(fd, &event, event.sizeof);
auto r = read(fd, &event, event.sizeof);
assert(r == event.sizeof);
// writef("\r%12s", event);
@ -891,7 +896,7 @@ version(linux) {
axes[event.number] = event.value >> 12;
}
if(event.type & JS_EVENT_BUTTON) {
buttons[event.number] = event.value;
buttons[event.number] = cast(ubyte) event.value;
}
writef("\r%6s %1s", axes[0..8], buttons[0 .. 16]);
stdout.flush();

10
jsvar.d
View File

@ -1,4 +1,14 @@
/*
FIXME:
overloads can be done as an object representing the overload set
tat opCall does the dispatch. Then other overloads can actually
be added more sanely.
FIXME:
instantiate template members when reflection with certain
arguments if marked right...
FIXME:
pointer to member functions can give a way to wrap things

1694
midi.d

File diff suppressed because it is too large Load Diff

3078
mp3.d Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,7 @@
/*
FIXME: i kinda do want a catch type filter.
REPL plan:
easy movement to/from a real editor
can edit a specific function

View File

@ -251,6 +251,15 @@ struct AudioOutputThread {
else static assert(0);
}
void playOgg()(string filename, bool loop = false) {
if(impl)
impl.playOgg(filename, loop);
}
void playMp3()(string filename) {
if(impl)
impl.playMp3(filename);
}
/// provides automatic [arsd.jsvar] script wrapping capability. Make sure the
/// script also finishes before this goes out of scope or it may end up talking
@ -436,6 +445,55 @@ final class AudioPcmOutThreadImplementation : Thread {
);
}
/// Requires mp3.d to be compiled in (module arsd.mp3) which is LGPL licensed.
void playMp3()(string filename) {
import arsd.mp3;
import std.stdio;
auto fi = File(filename);
scope auto reader = delegate(void[] buf) {
return cast(int) fi.rawRead(buf[]).length;
};
auto mp3 = new MP3Decoder(reader);
if(!mp3.valid)
throw new Exception("no file");
auto next = mp3.frameSamples;
addChannel(
delegate bool(short[] buffer) {
if(cast(int) buffer.length != buffer.length)
throw new Exception("eeeek");
more:
if(next.length >= buffer.length) {
buffer[] = next[0 .. buffer.length];
next = next[buffer.length .. $];
} else {
buffer[0 .. next.length] = next[];
buffer = buffer[next.length .. $];
next = next[$..$];
if(buffer.length) {
if(mp3.valid) {
mp3.decodeNextFrame(reader);
next = mp3.frameSamples;
goto more;
} else {
buffer[] = 0;
return false;
}
}
}
return true;
}
);
}
struct Sample {
int operation;
@ -503,6 +561,28 @@ final class AudioPcmOutThreadImplementation : Thread {
break;
/+
case 2: // triangle wave
short[] tone;
tone.length = 22050 * len / 1000;
short valmax = cast(short) (cast(int) volume * short.max / 100);
int wavelength = 22050 / freq;
wavelength /= 2;
int da = valmax / wavelength;
int val = 0;
for(int a = 0; a < tone.length; a++){
tone[a] = cast(short) val;
val+= da;
if(da > 0 && val >= valmax)
da *= -1;
if(da < 0 && val <= -valmax)
da *= -1;
}
data ~= tone;
for(; i < sampleFinish; i++) {
buffer[i] = val;
// left and right do the same thing so we only count
@ -519,7 +599,41 @@ final class AudioPcmOutThreadImplementation : Thread {
break;
case 3: // sawtooth wave
short[] tone;
tone.length = 22050 * len / 1000;
int valmax = volume * short.max / 100;
int wavelength = 22050 / freq;
int da = valmax / wavelength;
short val = 0;
for(int a = 0; a < tone.length; a++){
tone[a] = val;
val+= da;
if(val >= valmax)
val = 0;
}
data ~= tone;
case 4: // sine wave
short[] tone;
tone.length = 22050 * len / 1000;
int valmax = volume * short.max / 100;
int val = 0;
float i = 2*PI / (22050/freq);
float f = 0;
for(int a = 0; a < tone.length; a++){
tone[a] = cast(short) (valmax * sin(f));
f += i;
if(f>= 2*PI)
f -= 2*PI;
}
data ~= tone;
+/
case 5: // custom function
val = currentSample.f(currentSample.x);
@ -1175,6 +1289,29 @@ struct MidiOutputThread {
void popSong() {}
}
version(Posix) {
import core.sys.posix.signal;
private sigaction_t oldSigIntr;
void setSigIntHandler() {
sigaction_t n;
n.sa_handler = &interruptSignalHandler;
n.sa_mask = cast(sigset_t) 0;
n.sa_flags = 0;
sigaction(SIGINT, &n, &oldSigIntr);
}
void restoreSigIntHandler() {
sigaction(SIGINT, &oldSigIntr, null);
}
__gshared bool interrupted;
private
extern(C)
void interruptSignalHandler(int sigNumber) nothrow {
interrupted = true;
}
}
/// Gives MIDI output access.
struct MidiOutput {
version(ALSA) {
@ -1210,6 +1347,8 @@ struct MidiOutput {
static immutable ubyte[3] resetSequence = [0x0b << 4, 123, 0];
// send a controller event to reset it
writeRawMessageData(resetSequence[]);
static immutable ubyte[1] resetCmd = [0xff];
writeRawMessageData(resetCmd[]);
// and flush it immediately
snd_rawmidi_drain(handle);
} else version(WinMM) {
@ -1248,6 +1387,8 @@ struct MidiOutput {
/// Timing and sending sane data is your responsibility!
/// The data should NOT include any timestamp bytes - each midi message should be 2 or 3 bytes.
void writeRawMessageData(scope const(ubyte)[] data) {
if(data.length == 0)
return;
version(ALSA) {
ssize_t written;

View File

@ -9,7 +9,12 @@
* send messages without a recipient window
* setTimeout
* setInterval
*/
/*
Classic games I want to add:
* my tetris clone
* pac man
*/
/*
@ -75,6 +80,9 @@
`-Lgdi32.lib -Luser32.lib` on the build command. If you want the Windows
subsystem too, use `-L/subsystem:windows -L/entry:mainCRTStartup`.
If using ldc instead of dmd, use `-L/entry:wmainCRTstartup` instead of `mainCRTStartup`;
note the "w".
On Win32, you can pass `-L/subsystem:windows` if you don't want a
console to be automatically allocated.
@ -6913,8 +6921,8 @@ struct ScreenPainter {
}
//this function draws a circle with the drawEllipse() function above, it requires the upper left point and the radius
void drawCircle(Point upperLeft, int radius) {
drawEllipse(upperLeft, Point(upperLeft.x + radius, upperLeft.y + radius));
void drawCircle(Point upperLeft, int diameter) {
drawEllipse(upperLeft, Point(upperLeft.x + diameter, upperLeft.y + diameter));
}
/// .
@ -8184,6 +8192,7 @@ version(Windows) {
GetObject(i.handle, bm.sizeof, &bm);
// or should I AlphaBlend!??!?!
BitBlt(hdc, x, y, w /* bm.bmWidth */, /*bm.bmHeight*/ h, hdcMem, ix, iy, SRCCOPY);
SelectObject(hdcMem, hbmOld);
@ -8198,6 +8207,7 @@ version(Windows) {
GetObject(s.handle, bm.sizeof, &bm);
// or should I AlphaBlend!??!?!
BitBlt(hdc, x, y, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
SelectObject(hdcMem, hbmOld);

View File

@ -751,6 +751,8 @@ struct Terminal {
// almost always
if(t.indexOf("xterm") != -1)
t = "xterm";
if(t.indexOf("putty") != -1)
t = "xterm";
if(t.indexOf("tmux") != -1)
t = "tmux";
if(t.indexOf("screen") != -1)

421
wav.d Normal file
View File

@ -0,0 +1,421 @@
/++
Basic .wav file reading and writing.
History:
Written May 15, 2020, but loosely based on code I wrote a
long time ago, at least August 2008 which is the oldest
file I have generated from the original code.
The old code could only write files, the reading support
was all added in 2020.
+/
module arsd.wav;
import core.stdc.stdio;
/++
+/
struct WavWriter {
private FILE* fp;
/++
Opens the file with the given header params.
Make sure you pass the correct params to header, except,
if you have a seekable stream, the data length can be zero
and it will be fixed when you close. If you have a non-seekable
stream though, you must give the size up front.
If you need to go to memory, the best way is to just
append your data to your own buffer, then create a [WavFileHeader]
separately and prepend it. Wav files are simple, aside from
the header and maybe a terminating byte (which isn't really important
anyway), there's nothing special going on.
Throws: Exception on error from [open].
---
auto writer = WavWriter("myfile.wav", WavFileHeader(44100, 2, 16));
writer.write(shortSamples);
---
+/
this(string filename, WavFileHeader header) {
this.header = header;
if(!open(filename))
throw new Exception("Couldn't open file for writing"); // FIXME: errno
}
/++
`WavWriter(WavFileHeader(44100, 2, 16));`
+/
this(WavFileHeader header) @nogc nothrow {
this.header = header;
}
/++
Calls [close]. Errors are ignored.
+/
~this() @nogc {
close();
}
@disable this(this);
private uint size;
private WavFileHeader header;
@nogc:
/++
Returns: true on success, false on error. Check errno for details.
+/
bool open(string filename) {
assert(fp is null);
assert(filename.length < 290);
char[300] fn;
fn[0 .. filename.length] = filename[];
fn[filename.length] = 0;
fp = fopen(fn.ptr, "wb");
if(fp is null)
return false;
if(fwrite(&header, header.sizeof, 1, fp) != 1)
return false;
return true;
}
/++
Writes 8-bit samples to the file. You must have constructed the object with an 8 bit header.
Returns: true on success, false on error. Check errno for details.
+/
bool write(ubyte[] data) {
assert(header.bitsPerSample == 8);
if(fp is null)
return false;
if(fwrite(data.ptr, 1, data.length, fp) != data.length)
return false;
size += data.length;
return true;
}
/++
Writes 16-bit samples to the file. You must have constructed the object with 16 bit header.
Returns: true on success, false on error. Check errno for details.
+/
bool write(short[] data) {
assert(header.bitsPerSample == 16);
if(fp is null)
return false;
if(fwrite(data.ptr, 2, data.length, fp) != data.length)
return false;
size += data.length * 2;
return true;
}
/++
Returns: true on success, false on error. Check errno for details.
+/
bool close() {
if(fp is null)
return true;
// pad odd sized file as required by spec...
if(size & 1) {
fputc(0, fp);
}
if(!header.dataLength) {
// put the length back at the beginning of the file
if(fseek(fp, 0, SEEK_SET) != 0)
return false;
auto n = header.withDataLengthInBytes(size);
if(fwrite(&n, 1, n.sizeof, fp) != 1)
return false;
} else {
assert(header.dataLength == size);
}
if(fclose(fp))
return false;
fp = null;
return true;
}
}
version(LittleEndian) {} else static assert(0, "just needs endian conversion coded in but i was lazy");
align(1)
///
struct WavFileHeader {
align(1):
const ubyte[4] header = ['R', 'I', 'F', 'F'];
int topSize; // dataLength + 36
const ubyte[4] type = ['W', 'A', 'V', 'E'];
const ubyte[4] fmtHeader = ['f', 'm', 't', ' '];
const int fmtHeaderSize = 16;
const ushort audioFormat = 1; // PCM
ushort numberOfChannels;
uint sampleRate;
uint bytesPerSeconds; // bytesPerSampleTimesChannels * sampleRate
ushort bytesPerSampleTimesChannels; // bitsPerSample * channels / 8
ushort bitsPerSample; // 16
const ubyte[4] dataHeader = ['d', 'a', 't', 'a'];
uint dataLength;
// data follows. put a 0 at the end if dataLength is odd.
///
this(uint sampleRate, ushort numberOfChannels, ushort bitsPerSample, uint dataLengthInBytes = 0) @nogc pure @safe nothrow {
assert(bitsPerSample == 8 || bitsPerSample == 16);
this.numberOfChannels = numberOfChannels;
this.sampleRate = sampleRate;
this.bitsPerSample = bitsPerSample;
this.bytesPerSampleTimesChannels = cast(ushort) (numberOfChannels * bitsPerSample / 8);
this.bytesPerSeconds = this.bytesPerSampleTimesChannels * sampleRate;
this.topSize = dataLengthInBytes + 36;
this.dataLength = dataLengthInBytes;
}
///
WavFileHeader withDataLengthInBytes(int dataLengthInBytes) const @nogc pure @safe nothrow {
return WavFileHeader(sampleRate, numberOfChannels, bitsPerSample, dataLengthInBytes);
}
}
static assert(WavFileHeader.sizeof == 44);
/++
After construction, the parameters are set and you can set them.
After that, you process the samples range-style.
It ignores chunks in the file that aren't the basic standard.
It throws exceptions if it isn't a bare-basic PCM wav file.
See [wavReader] for the convenience constructors.
Note that if you are reading a 16 bit file (`bitsPerSample == 16`),
you'll actually need to `cast(short[]) front`.
---
auto reader = wavReader(data[]);
foreach(chunk; reader)
play(chunk);
---
+/
struct WavReader(Range) {
const ushort numberOfChannels;
const int sampleRate;
const ushort bitsPerSample;
int dataLength; // don't modify plz
private uint remainingDataLength;
private Range underlying;
private const(ubyte)[] frontBuffer;
static if(is(Range == CFileChunks)) {
this(FILE* fp) {
underlying = CFileChunks(fp);
this(0);
}
} else {
this(Range r) {
this.underlying = r;
this(0);
}
}
private this(int _initializationDummyVariable) {
this.frontBuffer = underlying.front;
WavFileHeader header;
ubyte[] headerBytes = (cast(ubyte*) &header)[0 .. header.sizeof - 8];
if(this.frontBuffer.length >= headerBytes.length) {
headerBytes[] = this.frontBuffer[0 .. headerBytes.length];
this.frontBuffer = this.frontBuffer[headerBytes.length .. $];
} else {
throw new Exception("Probably not a wav file, or else pass bigger chunks please");
}
if(header.header != ['R', 'I', 'F', 'F'])
throw new Exception("Not a wav file; no RIFF header");
if(header.type != ['W', 'A', 'V', 'E'])
throw new Exception("Not a wav file");
// so technically the spec does NOT require fmt to be the first chunk..
// but im gonna just be lazy
if(header.fmtHeader != ['f', 'm', 't', ' '])
throw new Exception("Malformed or unsupported wav file");
if(header.fmtHeaderSize < 16)
throw new Exception("Unsupported wav format header");
auto additionalSkip = header.fmtHeaderSize - 16;
if(header.audioFormat != 1)
throw new Exception("arsd.wav only supports the most basic wav files and this one has advanced encoding. try converting to a .mp3 file and use arsd.mp3.");
this.numberOfChannels = header.numberOfChannels;
this.sampleRate = header.sampleRate;
this.bitsPerSample = header.bitsPerSample;
if(header.bytesPerSampleTimesChannels != header.bitsPerSample * header.numberOfChannels / 8)
throw new Exception("Malformed wav file: header.bytesPerSampleTimesChannels didn't match");
if(header.bytesPerSeconds != header.bytesPerSampleTimesChannels * header.sampleRate)
throw new Exception("Malformed wav file: header.bytesPerSeconds didn't match");
this.frontBuffer = this.frontBuffer[additionalSkip .. $];
static struct ChunkHeader {
align(1):
ubyte[4] type;
uint size;
}
static assert(ChunkHeader.sizeof == 8);
ChunkHeader current;
ubyte[] chunkHeader = (cast(ubyte*) &current)[0 .. current.sizeof];
another_chunk:
// now we're at the next chunk. want to skip until we hit data.
if(this.frontBuffer.length < chunkHeader.length)
throw new Exception("bug in arsd.wav the chunk isn't big enough to handle and im lazy. if you hit this send me your file plz");
chunkHeader[] = frontBuffer[0 .. chunkHeader.length];
frontBuffer = frontBuffer[chunkHeader.length .. $];
if(current.type != ['d', 'a', 't', 'a']) {
// skip unsupported chunk...
drop_more:
if(frontBuffer.length > current.size) {
frontBuffer = frontBuffer[current.size .. $];
} else {
current.size -= frontBuffer.length;
underlying.popFront();
if(underlying.empty) {
throw new Exception("Ran out of data while trying to read wav chunks");
} else {
frontBuffer = underlying.front;
goto drop_more;
}
}
goto another_chunk;
} else {
this.remainingDataLength = current.size;
}
this.dataLength = this.remainingDataLength;
}
@property const(ubyte)[] front() {
return frontBuffer;
}
version(none)
void consumeBytes(size_t count) {
if(this.frontBuffer.length)
this.frontBuffer = this.frontBuffer[count .. $];
}
void popFront() {
remainingDataLength -= front.length;
underlying.popFront();
if(underlying.empty)
frontBuffer = null;
else
frontBuffer = underlying.front;
}
@property bool empty() {
return remainingDataLength == 0 || this.underlying.empty;
}
}
/++
Convenience constructor for [WavReader]
To read from a file, pass a filename, a FILE*, or a range that
reads chunks from a file.
To read from a memory block, just pass it a `ubyte[]` slice.
+/
WavReader!T wavReader(T)(T t) {
return WavReader!T(t);
}
/// ditto
WavReader!DataBlock wavReader(const(ubyte)[] data) {
return WavReader!DataBlock(DataBlock(data));
}
struct DataBlock {
const(ubyte)[] front;
bool empty() { return front.length == 0; }
void popFront() { front = null; }
}
/// Construct a [WavReader] from a filename.
WavReader!CFileChunks wavReader(string filename) {
assert(filename.length < 290);
char[300] fn;
fn[0 .. filename.length] = filename[];
fn[filename.length] = 0;
auto fp = fopen(fn.ptr, "rb");
if(fp is null)
throw new Exception("wav file unopenable"); // FIXME details
return WavReader!CFileChunks(fp);
}
struct CFileChunks {
FILE* fp;
this(FILE* fp) {
this.fp = fp;
buffer = new ubyte[](4096);
refcount = new int;
*refcount = 1;
popFront();
}
this(this) {
if(refcount !is null)
(*refcount) += 1;
}
~this() {
if(refcount is null) return;
(*refcount) -= 1;
if(*refcount == 0) {
fclose(fp);
}
}
//ubyte[4096] buffer;
ubyte[] buffer;
int* refcount;
ubyte[] front;
void popFront() {
auto got = fread(buffer.ptr, 1, buffer.length, fp);
front = buffer[0 .. got];
}
bool empty() {
return front.length == 0 && (feof(fp) ? true : false);
}
}