Add WavStream class

Like MP3Stream, but for WAV files.
diff --git a/32blit/32blit.hpp b/32blit/32blit.hpp
index 6849ad5..3cfe8b1 100644
--- a/32blit/32blit.hpp
+++ b/32blit/32blit.hpp
@@ -2,6 +2,7 @@
 
 #include "audio/audio.hpp"
 #include "audio/mp3-stream.hpp"
+#include "audio/wav-stream.hpp"
 #include "engine/api.hpp"
 #include "engine/engine.hpp"
 #include "engine/fast_code.hpp"
diff --git a/32blit/CMakeLists.txt b/32blit/CMakeLists.txt
index 8524813..9a4d195 100644
--- a/32blit/CMakeLists.txt
+++ b/32blit/CMakeLists.txt
@@ -1,6 +1,7 @@
 set(SOURCES
 	audio/audio.cpp
 	audio/mp3-stream.cpp
+	audio/wav-stream.cpp
 	engine/engine.cpp
 	engine/file.cpp
 	engine/api.cpp
diff --git a/32blit/audio/wav-stream.cpp b/32blit/audio/wav-stream.cpp
new file mode 100644
index 0000000..545c071
--- /dev/null
+++ b/32blit/audio/wav-stream.cpp
@@ -0,0 +1,283 @@
+#include <cinttypes>
+#include <cstring>
+
+#include "wav-stream.hpp"
+
+#include "audio/audio.hpp"
+#include "engine/engine.hpp"
+#include "engine/file.hpp"
+
+namespace blit {
+
+  struct WAVHeader {
+    char riff_id[4];
+    uint32_t riff_size;
+    char riff_format[4];
+
+    char fmt_id[4];
+    uint32_t fmt_size;
+    uint16_t fmt_format;
+    uint16_t fmt_channels;
+    uint32_t fmt_sample_rate;
+    uint32_t fmt_byte_rate;
+    uint16_t fmt_block_align;
+    uint16_t fmt_bits_per_sample;
+
+    char data_id[4];
+    uint32_t data_size;
+  };
+
+  WavStream::WavStream() {
+  }
+
+  WavStream::~WavStream() {
+    if(!file.get_ptr()) {
+      delete[] file_buffers[0];
+      delete[] file_buffers[1];
+    }
+  }
+
+  bool WavStream::load(std::string filename) {
+    if(channel != -1)
+      blit::channels[channel].off();
+
+    current_sample = nullptr;
+    buffered_samples = 0;
+
+    // avoid attempting to free later
+    if(file.get_ptr())
+      file_buffers[0] = file_buffers[1] = nullptr;
+
+    if(!file.open(filename))
+      return false;
+
+    // don't need a buffer if it's in flash
+    if(file.get_ptr()) {
+      delete[] file_buffers[0];
+      delete[] file_buffers[1];
+      file_buffers[0] = file_buffers[1] = nullptr;
+    }
+
+    // fill initial buffer
+    file_buffer_filled[0] = file_buffer_filled[1] = 0;
+    file_offset = 0;
+    file_data_len = 0;
+    read(0);
+
+    if(!file_buffer_filled[0])
+      return false;
+
+    // check header
+    auto head = reinterpret_cast<const WAVHeader *>(file_buffers[0]);
+
+    // some validation
+    if(memcmp(head->riff_id, "RIFF", 4) != 0 || memcmp(head->riff_format, "WAVE", 4) != 0 ||
+      memcmp(head->fmt_id, "fmt ", 4) != 0 || memcmp(head->data_id, "data", 4) != 0) {
+      return false;
+    }
+
+    // some restrictions
+    // just refusing to play anything that wastes space
+    if(head->fmt_format != 1 /*PCM*/ || head->fmt_channels != 1 || (head->fmt_sample_rate != 22050 && head->fmt_sample_rate != 11025))
+      return false;
+
+    bits_per_sample = head->fmt_bits_per_sample;
+    sample_rate = head->fmt_sample_rate;
+    file_data_len = head->data_size;
+
+    // advance buf past header
+    if(file.get_ptr()) {
+      // in-memory, adjust the ptr
+      file_buffers[0] += sizeof(WAVHeader);
+      file_buffer_filled[0] = file_data_len;
+    } else {
+      file_buffer_filled[0] -= sizeof(WAVHeader);
+      memmove(file_buffers[0], file_buffers[0] + sizeof(WAVHeader), file_buffer_filled[0]);
+    }
+
+    duration_ms = uint64_t(file_data_len) * 1000 / (sample_rate * bits_per_sample / 8);
+
+    // fill second buffer too
+    read(1);
+
+    return true;
+  }
+
+  void WavStream::play(int channel, int flags) {
+    if(!file_buffer_filled[0])
+      return;
+
+    pause();
+
+    this->channel = channel;
+    this->play_flags = flags;
+
+    if((flags & PlayFlags::from_start) && buffered_samples)
+      restart();
+
+    if(!current_sample) {
+      cur_play_buf = 0;
+      current_sample = file_buffers[0];
+      data_end = file_buffers[0] + file_buffer_filled[0];
+      blit::channels[channel].wave_buf_pos = 0;
+    }
+
+    blit::channels[channel].waveforms = blit::Waveform::WAVE;
+    blit::channels[channel].user_data = this;
+    blit::channels[channel].wave_buffer_callback = &WavStream::static_callback;
+
+    blit::channels[channel].trigger_sustain();
+  }
+
+  void WavStream::pause() {
+    if(channel != -1)
+      blit::channels[channel].off();
+  }
+
+  void WavStream::restart() {
+    bool was_playing = get_playing();
+    pause();
+
+    // reset file buffer
+    file_buffer_filled[0] = file_buffer_filled[1] = 0;
+    file_offset = 0;
+    read(0);
+
+    // reset sample buffer
+    current_sample = nullptr;
+    buffered_samples = 0;
+
+
+    if(was_playing)
+      play(channel, play_flags);
+  }
+
+  bool WavStream::get_playing() const {
+    return channel != -1 && blit::channels[channel].adsr_phase == blit::ADSRPhase::SUSTAIN;
+  }
+
+  int WavStream::get_play_flags() const {
+    return play_flags;
+  }
+
+  void WavStream::update() {
+    if(!current_sample)
+      return;
+
+    // refill file buffers
+    for(int i = 0; i < 2; i++) {
+      if(!file_buffer_filled[i])
+        read(i);
+    }
+  }
+
+  unsigned int WavStream::get_current_sample() const {
+    return buffered_samples + blit::channels[channel].wave_buf_pos;
+  }
+
+  int WavStream::get_duration_ms() const {
+    return duration_ms;
+  }
+
+  void WavStream::static_callback(AudioChannel &channel) {
+    reinterpret_cast<WavStream *>(channel.user_data)->callback(channel);
+  }
+
+  void WavStream::callback(AudioChannel &channel) {
+    if(!current_sample) {
+      channel.off();
+      return;
+    }
+
+    // there was no buffer last time
+    if(current_sample == data_end) {
+      if(file_buffer_filled[cur_play_buf] == -1) {
+        current_sample = data_end = nullptr;
+        channel.off();
+        return;
+      } else if(file_buffer_filled[cur_play_buf]) {
+        data_end = file_buffers[cur_play_buf] + file_buffer_filled[cur_play_buf]; // recovered from underrun
+      } else {
+        memset(channel.wave_buffer, 0, 64 * sizeof(int16_t));
+        return;
+      }
+    }
+
+    auto out = channel.wave_buffer;
+
+    int i = 0;
+    for(; i < 64 && current_sample; i++) {
+
+      if(bits_per_sample == 8) // 8-bit
+        *(out++) = (*current_sample << 8)  - 0x7F00;
+      else // 16-bit
+        *(out++) = *(const int16_t *)current_sample;
+
+      // the other rate is half, so only increment on even samples
+      if(sample_rate == 22050 || i & 1)
+        current_sample += bits_per_sample / 8;
+
+      // swap buffers
+      if(current_sample == data_end) {
+        file_buffer_filled[cur_play_buf] = 0;
+        cur_play_buf++;
+        cur_play_buf %= 2;
+
+        if(file_buffer_filled[cur_play_buf] == -1) // EOF
+          current_sample = data_end = nullptr;
+        else {
+          current_sample = file_buffers[cur_play_buf];
+          data_end = current_sample + file_buffer_filled[cur_play_buf];
+          // if(current_sample == end_sample) underrun
+        }
+      }
+    }
+
+    buffered_samples += i;
+
+    for(; i < 64; i++)
+      (*out++) = 0;
+  }
+
+  void WavStream::read(int buffer) {
+    // init buffer
+    if(file.get_ptr()) {
+      // we have the whole thing
+      if(buffer == 0 || (play_flags & PlayFlags::loop)) {
+        file_buffers[buffer] = const_cast<uint8_t *>(file.get_ptr());
+        file_buffer_filled[buffer] = file.get_length();
+
+        if(file_data_len) {
+          // skip header and set correct data size for loop
+          file_buffers[buffer] += sizeof(WAVHeader);
+          file_buffer_filled[buffer] = file_data_len;
+        }
+      }
+      return;
+    } else if(!file_buffers[buffer]) {
+      file_buffers[buffer] = new uint8_t[file_buffer_size];
+    }
+
+    // don't read past end of data chunk
+    uint32_t max_len = file_buffer_size;
+    if(file_data_len && file_data_len - file_offset < max_len)
+      max_len = file_data_len - file_offset;
+
+    auto read = file.read(file_offset, max_len, reinterpret_cast<char *>(file_buffers[buffer]));
+
+    if(read <= 0) {
+      if(play_flags & PlayFlags::loop) {
+        // retry at start
+        file_offset = sizeof(WAVHeader);
+        read = file.read(file_offset, file_buffer_size, reinterpret_cast<char *>(file_buffers[buffer]));
+      } else {
+        // else mark EOF
+        file_buffer_filled[buffer] = -1;
+        return;
+      }
+    }
+
+    file_buffer_filled[buffer] = read;
+    file_offset += read;
+  }
+}
diff --git a/32blit/audio/wav-stream.hpp b/32blit/audio/wav-stream.hpp
new file mode 100644
index 0000000..74d4e05
--- /dev/null
+++ b/32blit/audio/wav-stream.hpp
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <string>
+
+#include "audio/audio.hpp"
+#include "engine/file.hpp"
+
+namespace blit {
+
+  class WavStream final {
+  public:
+
+    enum PlayFlags {
+      from_start = (1 << 0),
+      loop       = (1 << 1)
+    };
+
+    WavStream();
+    ~WavStream();
+
+    bool load(std::string filename);
+
+    void play(int channel, int flags = 0);
+    void pause();
+    void restart();
+
+    bool get_playing() const;
+    int get_play_flags() const;
+
+    void update();
+
+    unsigned int get_current_sample() const;
+    int get_duration_ms() const;
+
+  private:
+    void read(int buffer);
+
+    static void static_callback(AudioChannel &channel);
+    void callback(AudioChannel &channel);
+
+    // file io
+    blit::File file;
+    uint32_t file_offset = 0;
+    uint32_t file_data_len = 0; // length of data from the header
+
+    static const int file_buffer_size = 1024;
+    uint8_t *file_buffers[2] = {nullptr, nullptr};
+    int32_t file_buffer_filled[2] = {0, 0};
+
+    int channel = -1;
+    int play_flags = 0;
+
+    // decoding
+    const uint8_t *data_end, *current_sample;
+    uint8_t bits_per_sample;
+    uint16_t sample_rate;
+    int cur_play_buf = 0;
+
+    unsigned int buffered_samples = 0;
+    int duration_ms = 0;
+  };
+}