Skip to content

Commit

Permalink
Merge pull request #7 from vladkorotnev/experiment-polyphony
Browse files Browse the repository at this point in the history
Full 1 bit wave output engine rather than PWM
  • Loading branch information
vladkorotnev authored Aug 4, 2024
2 parents e7926f5 + f8b49c8 commit 2cdf168
Show file tree
Hide file tree
Showing 57 changed files with 8,479 additions and 2,407 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,13 @@ You can also read the quest I went through trying to get it to run "in real time
* [Cream puff — Mermaid girl](https://youtu.be/AaUMvgfHpUo?t=16): [MIDI](helper/chimes/mermgrl.mid)
* [Brisk & Trixxy — Eye Opener](https://youtu.be/81QqHUpyBhg?t=83): [MIDI](helper/chimes/eye_opener.mid)
* [Hiroyuki Oshima - The Ark Awakes From The Sea Of Chaos](https://www.youtube.com/watch?app=desktop&v=cB7eevDk1s0): [MIDI](helper/chimes/ark.mid)
* [Timbaland - Give It To Me](https://youtube.com/watch?v=RgoiSJ23cSc) also known as [Skibidi Toilet](https://youtu.be/6dMjCa0nqK0): [MIDI](helper/chimes/skibidi_toilet.mid)
* [PinocchioP - God-ish (神っぽいな)](https://www.youtube.com/watch?v=EHBFKhLUVig): [MIDI](helper/chimes/kamippoina.mid)

MIDI to sequencer conversion tool (supports note events in one track only, track end event, and comment event): [midi_to_chime](helper/midi_to_chime.py)

8 bit 8 kHz wave to RLE sample conversion tool (not even reading the header, so very jank): [pwm.py](helper/pwm.py)

## Remote Control Server

There is a remote control server you can enable in settings for debugging remotely when uploading firmware via OTA, or using an emulator without any screen and buttons.
Expand Down Expand Up @@ -172,7 +176,7 @@ The basic configuration without any bluetooth functionality (no Switchbot or Bal

### Speaker (at least one required)

* Piezo speaker ([driver](src/sound/beeper.cpp), [music](src/sound/melodies.cpp))
* Piezo speakers: *now with 1-bit DMA polyphony!* ([driver](src/sound/beeper.cpp), [music](src/sound/melodies.cpp), [sequencer](src/sound/sequencer.cpp))

### Haptics (WIP)

Expand Down
Binary file modified helper/chimes/arise.mid
Binary file not shown.
Binary file modified helper/chimes/ark.mid
Binary file not shown.
Binary file modified helper/chimes/bouken.mid
Binary file not shown.
Binary file modified helper/chimes/duvet.mid
Binary file not shown.
Binary file modified helper/chimes/gammapolisz.mid
Binary file not shown.
Binary file modified helper/chimes/gentlejena.mid
Binary file not shown.
Binary file added helper/chimes/kamippoina.mid
Binary file not shown.
Binary file added helper/chimes/skibidi_toilet.mid
Binary file not shown.
73 changes: 46 additions & 27 deletions helper/midi_to_chime.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,73 @@
#!/usr/bin/env python3

# this is very jank, do not expect it to work as is
# it worked for some melodies though

import pdb
from sys import argv
from mido import MidiFile
import freq_note_converter

mid = MidiFile(argv[1])
name = argv[2]

last_time = 0
evts = [] # of (freq in hz or 0, delay in ms)
ended = False

class Event():
def __init__(self, kind, chan, arg):
self.kind = kind
self.chan = chan
self.arg = arg

def __str__(self):
return f" {{{self.kind}, {str(self.chan)}, {str(int(self.arg))}}},"

class Comment():
def __init__(self, s):
self.content = s
self.kind = "REM"

def __str__(self):
return f" /* {self.content} */"

evts = []

def prev_note_off_event(chan):
for i in range(1,len(evts)+1):
e = evts[-i]
if e.kind == "FREQ_SET" and e.arg == 0 and e.chan == chan:
return e
elif e.kind == "DELAY":
return None
return None

for msg in mid:
print(msg)
if msg.type == "note_on" or msg.type == "note_off":
print(msg)
if msg.time > 0 and len(evts) > 0 and evts[-1][1] == 0:
evts[-1][1] = int(msg.time * 1000)
if msg.time > 0.005:
evts.append(Event("DELAY", 0, msg.time * 1000))
if msg.type == "note_on" and msg.velocity > 0:
evts.append([int(freq_note_converter.from_note_index(msg.note).freq), 0, ""])
existing_evt = prev_note_off_event(msg.channel)
if existing_evt is not None:
existing_evt.arg = freq_note_converter.from_note_index(msg.note).freq
else:
evts.append(Event("FREQ_SET", msg.channel, freq_note_converter.from_note_index(msg.note).freq))
else:
# note off
evts.append([0, 0, ""])
evts.append(Event("FREQ_SET", msg.channel, 0))
elif msg.type == "end_of_track":
print(msg)
if ended:
raise Exception("WTF, already ended")
ended = True
if evts[-1][0] == 0:
# pause exists, just extend it
evts[-1][1] = int(msg.time * 1000)
else:
evts.append([0, int(msg.time*1000), ""])
evts.append(Event("DELAY", 0, msg.time * 1000))
elif msg.type == "marker":
evts[-1][2] = msg.text

if msg.time > 0.005:
evts.append(Event("DELAY", 0, msg.time * 1000))
evts.append(Comment(msg.text))
if msg.text == "LOOP":
evts.append(Event("LOOP_POINT_SET", 0, 0))

print(evts)

print("static const melody_item_t "+name+"_data[] = {")
i = 0
while i < len(evts):
if evts[i][0] != 0 or evts[i][1] != 0:
print(" {"+str(evts[i][0])+", "+str(evts[i][1])+"}, ")
if evts[i][2] != "":
print(" ")
print(" // " + evts[i][2])
i+=1
for e in evts:
print(str(e))
print("};")

print("const melody_sequence_t "+name+" = MELODY_OF("+name+"_data);")
60 changes: 60 additions & 0 deletions helper/pwm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#-*- coding: utf-8 -*-

# Super jank converter from 8bit WAV into RLE PWM + preview
# Not reading the header or anything hence super jank

import sys

MARGIN = 4

fname = sys.argv[1]
sname = sys.argv[2]
oname = sys.argv[3] if len(sys.argv) >= 4 else None
sdata = open(fname, 'rb').read()
outf = None
if oname is not None:
outf = open(oname, 'wb')
outf.write(sdata[:0x28])

sdata = sdata[0x28::]
i = 0
min = 999
max = 0
sts = 0xFF
last_sts = 0xFF
rle_buf = [0]

def median(data):
x = list(data)
x.sort()
mid = len(x) // 2
return (x[mid] + x[~mid]) / 2.0

med = median(sdata)
print("Median", med)
HIGH = med + MARGIN
LO = med - MARGIN

while i < len(sdata):
curSample = sdata[i]
if curSample >= HIGH:
sts = 255
elif curSample <= LO:
sts = 1
if curSample < min and curSample > 0:
min = curSample
if curSample > max and curSample > 0:
max = curSample
if outf is not None:
outf.write(bytes([sts, sts]))
if sts != last_sts:
rle_buf.append(0)
last_sts = sts
if rle_buf[-1] == 255:
rle_buf.append(0)
rle_buf.append(0)
rle_buf[-1] += 1
i += 2

print(f"static const uint8_t {sname}_rle_data[] = {{" + str(rle_buf)[1::][:-1:] + "};")
print(f"static const rle_sample_t {sname} = {{ .sample_rate = 4000, .root_frequency = 524 /* C5 */, .rle_data = {sname}_rle_data, .length = {len(rle_buf)} }};")
4 changes: 3 additions & 1 deletion include/app/alarm_editor.h
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
#pragma once
#include <views/framework.h>
#include <sound/beeper.h>
#include <sound/sequencer.h>
#include <service/alarm.h>
#include "proto/navmenu.h"

class AppShimAlarmEditor: public ProtoShimNavMenu {
public:
AppShimAlarmEditor(Beeper*);
AppShimAlarmEditor(Beeper*, NewSequencer*);
void pop_renderable(transition_type_t = TRANSITION_SLIDE_HORIZONTAL_RIGHT);

private:
class AlarmEditorView;
Beeper * beeper;
NewSequencer * sequencer;

int current_editing_idx;
alarm_setting_t current_editing_setting;
Expand Down
11 changes: 6 additions & 5 deletions include/app/alarming.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@
#include <views/framework.h>
#include <graphics/framebuffer.h>
#include <sound/beeper.h>
#include <sound/sequencer.h>
#include <sensor/sensor.h>

void app_alarming_prepare(Beeper*);
void app_alarming_prepare(NewSequencer*);
void app_alarming_draw(FantaManipulator*);
void app_alarming_process();

class AppShimAlarming: public Renderable {
public:
AppShimAlarming(Beeper*b) {
beeper = b;
AppShimAlarming(NewSequencer*b) {
sequencer = b;
}

void prepare() {
app_alarming_prepare(beeper);
app_alarming_prepare(sequencer);
}

void render(FantaManipulator*fb) {
Expand All @@ -26,5 +27,5 @@ class AppShimAlarming: public Renderable {
app_alarming_process();
}
private:
Beeper *beeper;
NewSequencer *sequencer;
};
7 changes: 4 additions & 3 deletions include/app/idle.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
#include <views/framework.h>
#include <graphics/framebuffer.h>
#include <sound/beeper.h>
#include <sound/sequencer.h>
#include <sensor/sensor.h>

void app_idle_prepare(SensorPool*, Beeper*);
void app_idle_prepare(SensorPool*, Beeper*, NewSequencer*);
void app_idle_draw(FantaManipulator*);
void app_idle_process();

class AppShimIdle: public Renderable {
public:
AppShimIdle(SensorPool*sp, Beeper*b) {
app_idle_prepare(sp, b);
AppShimIdle(SensorPool*sp, Beeper*b, NewSequencer*s) {
app_idle_prepare(sp, b, s);
}

void render(FantaManipulator*fb) {
Expand Down
3 changes: 2 additions & 1 deletion include/app/menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
#include <views/framework.h>
#include <graphics/framebuffer.h>
#include <sound/beeper.h>
#include <sound/sequencer.h>
#include <sensor/sensor.h>

class AppShimMenu: public ProtoShimNavMenu {
public:
AppShimMenu(Beeper*);
AppShimMenu(Beeper*, NewSequencer*);

void prepare();
void step();
Expand Down
2 changes: 1 addition & 1 deletion include/app/timer_editor.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class AppShimTimerEditor: public ProtoShimNavigationStack {
public:
AppShimTimerEditor(Beeper *);
AppShimTimerEditor(Beeper *, NewSequencer *);
~AppShimTimerEditor();
private:
class TimerEditorMainScreen;
Expand Down
10 changes: 9 additions & 1 deletion include/device_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@
#define HAS(x) defined(HAS_##x)

// ---- SOFTWARE FEATURE FLAGS

// Enable the Wordnik API and Word Of The Day screen. UNSTABLE: Uses a lot of RAM for HTTPS.
#define HAS_WORDNIK_API

// Enable over-the-air firmware version upgrade. NB: Requires a new partition map, someday later
// #define HAS_OTAFVU

// Enable Switchbot Meter temperature probing. UNSTABLE: Uses NimBLE so a lot of RAM, disconnects over time.
// #define HAS_SWITCHBOT_METER_INTEGRATION
#define HAS_BALANCE_BOARD_INTEGRATION

// Enable Wii Balance Board measuring. UNSTABLE: Uses Bluedroid (a FUCKTON of RAM), periodic disconnects or reboots without leaving a stack trace.
// #define HAS_BALANCE_BOARD_INTEGRATION

// ---- HARDWARE

Expand Down
2 changes: 1 addition & 1 deletion include/devices/big_clock.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

// Plasma Information System OS (not DOS, there's no disk in it!)
#define PRODUCT_NAME "PIS-OS"
#define PRODUCT_VERSION "2.3"
#define PRODUCT_VERSION "3.0"

// ---- Connection to DISP BOARD ----
const gpio_num_t HWCONF_PLASMA_DATABUS_GPIOS[] = {
Expand Down
2 changes: 1 addition & 1 deletion include/devices/smol_clock.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

// Plasma Information System OS (not DOS, there's no disk in it!)
#define PRODUCT_NAME "microPIS-OS"
#define PRODUCT_VERSION "2.3"
#define PRODUCT_VERSION "3.0"

// ---- Connection to beeper ----
const gpio_num_t HWCONF_BEEPER_GPIO = GPIO_NUM_12;
Expand Down
3 changes: 2 additions & 1 deletion include/display/md_plasma.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class MorioDenkiPlasmaDriver: public DisplayDriver {
const gpio_num_t blanking,
const gpio_num_t hv_enable
);

void initialize();

/// @brief Reset the display controller
void reset();
Expand All @@ -44,7 +46,6 @@ class MorioDenkiPlasmaDriver: public DisplayDriver {
gpio_num_t bright_gpio;
gpio_num_t show_gpio;
gpio_num_t hv_en_gpio;
void initialize();
inline void set_databus(uint8_t data);
inline void pulse_clock();
/// @brief Send a half-column to the display controller
Expand Down
2 changes: 1 addition & 1 deletion include/display/ws0010.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Ws0010OledDriver: public DisplayDriver {
const gpio_num_t en
);

void initialize();
/// @brief Reset the display controller
void reset();
void clear();
Expand All @@ -41,7 +42,6 @@ class Ws0010OledDriver: public DisplayDriver {
bool is_writing_ddram;
uint8_t ddram_ptr;

void initialize();
inline void set_databus(uint8_t data);
inline void set_is_command(bool);
inline void pulse_clock();
Expand Down
1 change: 1 addition & 0 deletions include/graphics/display_driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

class DisplayDriver {
public:
virtual void initialize();
/// @brief Reset the display controller
virtual void reset();
/// @brief Send an array of half-columns to the display controller
Expand Down
6 changes: 3 additions & 3 deletions include/network/otafvu.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
/// @brief Manages the ArduinoOTA update service
class OTAFVUManager {
public:
OTAFVUManager(Console*, BeepSequencer*);
OTAFVUManager(Console*, NewSequencer*);
~OTAFVUManager();

/// @brief Perform pending work of the service. Normally this is called by the internal task of the service and doesn't need to be called externally.
void task();
private:
Console *con;
BeepSequencer *seq;
NewSequencer *seq;

/// @brief Prepare to install an OTA update
void get_ready();
Expand All @@ -28,7 +28,7 @@ class OTAFVUManager {
#else
class OTAFVUManager {
public:
OTAFVUManager(Console*, BeepSequencer*) {}
OTAFVUManager(Console*, NewSequencer*) {}
~OTAFVUManager() {}
};
#endif
Loading

0 comments on commit 2cdf168

Please sign in to comment.