sobota 28. prosince 2013

Detekce hudebních akordů regulárním výrazem

V posledních několika měsících jsem programoval aplikaci pro zobrazování písniček včetně akordů. Akordy byly buď označeny zvláštní značkou (například v závorkách) nebo jen jako součást textu a mým úkolem bylo akord rozeznat pro potenciální transpozici. Transpozice je posunutí všech tónů v písničce o stejný interval. Tím způsobem se pro písničku změní tónina. Pro to je potřeba napřed každý akord správně detekovat a všechny jeho části.

Struktura zápisu akordu

Akordy se zapisůjí různě a pro jeden akord existuje často mnoho zápisů. Následující odkazy obsahují možný výčet akordů:

http://www.supermusic.sk/akordy.php http://www.akordytexty.cz/

Ze zápisů jsem vybral několik akordů:

Cmi6add9 Cmimaj7 C7maj3/A

Není ani tak nutné vědět co akord znamená, ale poznat co je akord a co obsahuje za tón (tóny).

Z příkladů akordů jsem rozdělil akord na několik částí - Tón akordu (C) + modifikace (mi6add9, mimaj7, 7maj3) + / + 2. tón akordu (A)

Tón akordu

Tón akordu je písmeno, které reprezentuje tón. Písmeno může následovat ještě posunutí (béčko, křížek, dvojté béčko, dvojitý křížek). Tón akordu může být písmeno A-H. Křížek - zvednutí i půltón se reprezentuje znakem sharp (#), béčko se reprezentuje zpravidla malým b, případně příponou "es" (u tónů A a E je to jen přípona "s"). V českém zápise po tónu A následuje tón H a jeho snížení o půltón je B, neexistuje snížený tón "Hes". V západním zápise neexistuje tón H a nahrazuje ho tón B, jeho snížení je potom Bb. Je tedy faktický půltonový rozdíl mezi západním B a českým B! U dvojitého snížení je to potom přípona "eses", případně "sas" pro A a "ses" pro E. Regulární výraz pro tón je potom následující:

[ABCDEFGHabcdefgh](\#|b|ses|sas|es){0,2}

Tento výraz má v sobě určitou volnost, protože detekuje i akordy jako třeba "Asassas", detekuje ale všechny možné validní varitanty. Pro přesnější zápis by bylo třeba pro A a E vytvořit výjimky. V případě, že se používá pro zápis jen křížek, béčko a dvojité snížení a zvýšení nehrozí (protože se v praxi na zápis u písniček nezapisují - nemají smysl), regulární výraz se dá zjednodušit na:

[ABCDEFGHabcdefgh](\#|b)?

Modifikace akordu

Modifikace obsahují zpravidla trojpísmenové heslo nebo číslo (1-2 ciferné). Někdy obojí. Některé akordy mají obojí a dvakrát za sebou (mi6add9). Pro pojmutí takové mofikace je nutné mít výčet možných hesel (z odkazů výše) a pak 1-2číslí tyto hesla s číslicemi se mohou opakovat 0-2krát. Regulární výraz pro modifikaci potom vypadá následovně:

((maj|mi|sus|add|m)[1-9]{0,2}){0,2}

Druhý tón

Některé akordy poté ještě obsahují lomítko a další tón. Tento tón má stejná pravidla jako první tón, akorát je následován lomítkem, opět mohou následovat modifikace akordu, i když ne v takovém množství:

(\/[ABCDEFGHabcdefgh](\#|b)?)?

Výsledný výraz

Z předchozích úvah se dá vytvořit složitější nebo jednodušší verze výrazu. Liší se počtem možných zápisů, které výraz přijme, ale také počtem chybných, které výraz přijme. Já osobně používám následující, protože akordy jsou výhradně české, neobsahují dvojité snížení aní zvýšení:

([A-Habcdefgh]\#?b?)(?:sus|maj|mimaj|add|aug|dim|min|mi|m|b|\+)?[1-9]{0,2}(?:(\/)
([A-Habcdefgh]\#?b?)(?:sus|maj|mimaj|add|aug|dim|min|mi|m|b|\+)?[1-9]{0,2})?

Tento výraz mam rozdělený na několik částí, sestavení v jazyce java vypadá následovně:

 private static final String SINGLE_TONE = "[A-Habcdefgh]\\#?b?"; // b

 private static final String TONE_ADDITIONS = "(?:sus|maj|mimaj|add|aug|dim|min|mi|m|b|\\+)?[1-9]{0,2}";

 public static final String FULL_CHORD = "(" + SINGLE_TONE + ")" + TONE_ADDITIONS + "(?:(\\/)(" +
SINGLE_TONE + ")" + TONE_ADDITIONS + ")?";
Edit:

Výsledný výraz používám v aplikaci Zpěvník pro android, více o aplikaci na http://karel-hovorka.eu/zpevnik/.

pátek 4. května 2012

Změna url webu pomocí javascriptu od nerozeznání od webu bez js

Změna url webu pomocí javascriptu od nerozeznání od webu bez js

Úvod

HTML5 umožňuje změnu url v rámci domény. To dříve nešlo a obcházelo se to většinou pomocí url za hashmarkem (#), protože to je část, která se mění jen v rámci stránky, ale neposílá se na server.

A k čemu to vlastně bude? Touto metodou se stránka může chovat jako by staticky přepínala stránky a přitom bude stránku ovládat javascript a dotahovat stránku na pozadí, samozřejmě s fallbackem pokud prohlížeč nebude pushState podporovat nebo bude mít vyplý javascript. Chování je pro uživatele opticky stejné, ale z programátorského hlediska může být tento způsob výhodnější - žádný refresh.

Volání je jednoduché, stačí zavolat metodu pushState objektu history:

history.pushState(objekt, nazev, url);

Metoda mimojiné kromě změny url (která je nepovinná) mění "stav" aplikace nastavením aktuálního stavového objektu a jeho názvu. Aktuální stavový objekt je uložen v proměnné

history.state

Nám ale bude stačit fukčnost změny url. Více informací o stavech historie je možné se dočíst ve specifikaci

Struktura stránky

Tady je příklad jak může vypadat stránka, u které chceme použít navigace:

<!DOCTYPE html>
<html lang="cs">
<head>
  <meta charset="utf-8">
  <title>titulek</title>
</head>
<body>
  <header id="header">
    <h1>hlavni nadpis</h1>
  </header>
  <nav id="nav">
    <ul>
      <li class="vybrany"><a href="uvod.html">Úvod</a></li>
      <li><a href="zivotopis.html">Životopis</a></li>
      <li><a href="projekty.html">Projekty</a></li>
      <li><a href="dalsi.html">Další záliby</a></li>
    </ul>
  </nav>
  <section id="content">
    <h2>Úvod</h2>
    <p>Now that there is the Tec-9, a crappy spray gun from South Miami. This gun is advertised as the most popular gun in American crime. Do you believe that shit? It actually says that in the little book that comes with it: the most popular gun in American crime. Like they're actually proud of that shit.  </p>
  </section>
  <footer id="footer">
    &copy; 2012 <a href="mailto:#">Karel Hovorka</a>
  </footer>
</body>
</html>

Navázání události na kliknutí

Kromě samotného javascriptu si pomůžeme knihovnou jQuery. Chceme odchytit klik na každý odkaz v elementu #nav, toho docílíme následujícím:

    $(aSelector).each (function() {
      $(this).bind("click", function (e) {
        //TODO: klik
      });
    });

Výkonný kód musí dotáhnout data pomocí zvoleného odkazu a aktualizovat potřebná data je potřeba nějakým způsobem roztřídit, na to zvolíme css selektory - my budeme chtít aktualizovat hlavní nadpis (#header h1), obsah (#content) a titulek (title).

Ajax a parsování dat

Parsováná data se napřed zkrátí jen na část uvnitř body a obsah elementu title, aby se zbytečně neimportovali a nespouštěli css nebo skripty (skripty uvnitř elementu body se budou pouštět). Volání ajax requestu a první parsování dat vypadá následovně:

  var request = $.get(href, function(data) {
    data = data.substring(data.indexOf("<body>") + 6, data.lastIndexOf("</body>"));
    var title = data.substring(data.indexOf("<title>") + 7, data.lastIndexOf("</title>"));
    var bodyDom = $($('<div></div>').append(data));
    //TODO: aktualizovat data
    window.history.pushState("object" + href, "title" + href, href);
  });

Aktualizace dat

Po poslední části kódu už je k dispozici DOM obsahu dotáhlého dokumentu a obsah elementu title, teď akorát chybí data aktualizovat:

var contentSelectors = ["#header h1", "#content", "title"];
var selector;
for (i = 0; i < contentSelectors.length; i++) {
  selector = contentSelectors[i];
  if (selector == "title") {
    document.title = title;
  } else {
    $(selector).html($(selector, bodyDom).html());
  }
}

Aktualizace odkazů

Poslední věc, která se často ještě mění je třída položky seznamu, která je zrovna vybrána, toto se aktualizuje následujícím kódem, který napřed třídu vymaže všem existujícím a následně nastaví aktuálním:

  var clazz;
  $("." + clazz).each (function() {
    $(this).removeClass(clazz);
  });
  $("a[href='" + href + "']").each (function() {
    $(this).parent("li").addClass(clazz);
  });

Toto jsou snad všechny základní myšlenky. Ještě se musí na začátku zjistit jestli prohlížeč vůbec pushState podporuje a navázat události click jen pokud ano. Pak se také musí kontrolovat, že klik byl levým tlačítkem. Nasazení naostro můžete vidět na karel.hovorka.net.

Výsledný kód

Výsledný kód po úpravách a dekompozici:

function HTML5Nav(contentSelectors, clazz) {
  this.clazz = clazz;
  this.contentSelectors = contentSelectors;
}

HTML5Nav.prototype.load = function (aSelector) {
  var globalThis = this;
  if (window.history.pushState) {
    $(aSelector).each (function() {
      $(this).bind("click", function (e) {
        globalThis._click(e);
      });
    });
  }
}

HTML5Nav.prototype._click = function(e) {
  if (e.which == 1) {
    var a = $(e.target);
    var href = a.attr("href");
    this.changePage(href);
    e.preventDefault();
  }   
}

HTML5Nav.prototype.changePage = function(href) {
  var globalThis = this;
  var request = $.get(href, function(data) {
    data = data.substring(data.indexOf("<body>") + 6, data.lastIndexOf("</body>"));
    var title = data.substring(data.indexOf("<title>") + 7, data.lastIndexOf("</title>"));
    var bodyDom = $($('<div></div>').append(data));
    globalThis._ajaxResponse(href, bodyDom, title);
  });
}

HTML5Nav.prototype._ajaxResponse = function(href, bodyDom, title) {
    var selector;
    for (i = 0; i < this.contentSelectors.length; i++) {
      selector = this.contentSelectors[i];
      if (selector == "title") {
        document.title = title;
      } else {
        $(selector).html($(selector, bodyDom).html());
      }
    }
    window.history.pushState("object" + href, "title" + href, href);
    this._setSelection(href, this.clazz);
}

HTML5Nav.prototype._setSelection = function(href) {
  var clazz = this.clazz;
  $("." + clazz).each (function() {
    $(this).removeClass(clazz);
  });
  $("a[href='" + href + "']").each (function() {
    $(this).parent("li").addClass(clazz);
  });
}

Příklad volání

První parametr jsou selektory elementů, které se mají aktualizovat, druhý parametr je třída, kterou mají mít položky seznamu vybraného odkazu. Metoda load naváže na odkazy událost kliku, jako parametr bere selektor na odkazy, na které chcem vázat odkazy. Příklad volání kódu :

var nav = new HTML5Nav(["#header h1", "#content", "title"], "vybrany");
window.onload = nav.load("#nav a");

Odkazy

čtvrtek 8. prosince 2011

Multiplatformní Sockety a TCP pro Windows a Linux v C/C++

Úvod

Programování TCP v C je rozdílné ve Windows a Linuxu. Existují wrappery a různé knihovny, které funkcionalitu sjednocují a zlepšují. Rozdíly jsou ale tak malé, že by mohli být řešeny preprocesorem, které by mělo několik výhod:

  • Kód se může psát nativně pro jednu platformu.
  • Direktivy budou krátké a jednoduché.
  • Zkompilovaný bude rychlý, protože nebude žádná další režie (wrappery).

Hlavičkové soubory

Každý systém má vlastní sadu hlavičkových souborů, které jsou nutné pro funkcionalitu socketů:

Windows

Existuje několik možných hlavičkových souborů mimo tento (windows.h, Winsock.h).

#include <WinSock2.h>

Linux

#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

Porovnání funkcí

Porovnával jsem následující funkce:


  • socket
  • bind
  • listen
  • accept
  • send
  • recv
  • close (closesocket)

Funkce socket

Definice

Windows

SOCKET socket(int af, int type, int protocol);

Linux

int socket(int domain, int type, int protocol);

Parametry

Parametry jsou stejného datového typu.

Návratová hodnota

Z definice funkce se zdá, že jediným rozdílem je návratová hodnota, která je ve Windows typu SOCKET a linuxu typu int.
Ve winsock.h je SOCKET definován takto:

typedef u_int           SOCKET;
A u_int následně takto:
typedef unsigned int    u_int;
Je to tedy unsigned int a int.

Úspěch:
V případě úspěchu funkce vrací kladné číslo vyjadřující deskriptor daného socketu pro oba operační systémy.
Chybový stav:

Windows

Vrací konstantu INVALID_SOCKET, která je definována:
#define INVALID_SOCKET  (SOCKET)(~0)

Linux

Vrací -1, což je binární vyjádření hodnoty ~0

Funkce bind

Definice

Windows

int bind(SOCKET s, const struct sockaddr *name, int namelen);

Linux

int bind(int s, struct sockaddr *my_addr, socklen_t addrlen);

Parametry

První parametr je rozdílný - SOCKET vs int. Rozdíly byly popsány u návratové hodnoty funkce socket.
V linuxu je třetí parametr typu socklen_t, ve windows je to int. Parametr obsahuje velikost truktury sockaddr* druhého parametru. Pokud ho získáme pomocí funkce sizeof("2. parametr"), v linuxu se vrátí datový typ kompatibilní s socketlen_t (nenašel jsem přesnou dokumentaci) a program se bez problémů zkompiluje.

Návratová hodnota

Návratová hodnota je 0 v případě úspěchu. V případě neúspěchu je v linuxu vrácena hodnota -1 a ve windows konstanta SOCKET_ERROR, která je taky -1.

Funkce listen

Definice

Windows

int listen(SOCKET s, int backlog);

Linux

int listen(int s, int backlog);

Parametry

Podobně jako u funkce bind je zde rozdílný datový typ pro socket.

Funkce accept

Definice

Windows

SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen); 

Linux

int accept(int s, struct sockaddr *addr, socklen_t *address_len);

Parametry

Podobně jako u funkce bind je zde rozdílný datový typ pro socket. Další parametry jsou stejné jako u funkce bind s tím rozdílem, že tentokrát se posílají pointery.
Zde může nastat problém, protože některé překladače v linuxu nepovolí třetí parametr typu int bez explicitního přetypování (g++).

Návratová hodnota

Stejně jako u funkce socket.


Funkce send

Definice

Windows

int send(SOCKET s, const char *buf, int len, int flags);

Linux

int send(int s, const void *buf, size_t len, int flags); 

Parametry

Stejný problém s datovým typem socketu a datovým typem pro velikost (size_t vs int).

Návratová hodnota

Windows

Počet odeslaných bytů, případně SOCKET_ERROR (-1) pokud nastala chyba.

Linux

Počet odeslaných bytů, případně -1.


Funkce recv

Definice

Windows

int recv(SOCKET s, char *buf, int len, int flags);

Linux

int recv(int s, void *buf, size_t len, int flags);

Parametry

Stejný problém s datovým typem socketu a datovým typem pro velikost (size_t vs int).

Návratová hodnota

Počet přijatých bytů, případně SOCKET_ERROR (-1) pokud nastala chyba. 0 pokud byl socket ukončen standardně.


Funkce close

Definice

Tady je problém přímo v definici - názvy nesouhlasí.

Windows

int closesocket(SOCKET s);

Linux

int close(int fd);

Parametry

Stejný problém s datovým typem socketu.

Návratová hodnota

0 v případě úspěchu, SOCKET_ERROR (-1) pokud nastala chyba.

Windows - specifické funkce

Ve windows se navíc používají funkce na inicializaci a ukončení používání socketů, mezi základní patří funkce:
WORD MAKEWORD(BYTE bLow, BYTE bHigh); (makro)
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
int WSACleanup(void);
struktura WSADATA (LPWSADATA)
Toto je jen nejzákladnější výčet, funkcí existuje víc. Tyto funkce jsou platformně velmi specifické, ale v jednoduchých případech není potřeba udělat nic víc než startup a cleanup.

Řešení

Mým snažením je udělat řešení tak, aby se dala aplikace psát nativně pro jednu platformu a pomocí makra byla funkční i v druhé platformě. Protože na windows je nutné používat WSA funkce, rozhodl jsem se tuto platformu použít jako nativní.

Odstranění problémů s WSA funkcemi

V linuxu není nutné tyto funkce volat, proto stačí jen vytvořit funkce s prázdným tělem vracející defaultní hodnoty, případně návratové hodnoty úspěchu.

Odstranění rozdílných datových typů pro socket

Protože nativní platformou je Windows, nejjednodušším způsobem je dodefinováním datového typu SOCKET.

Odstranění problému s socklen_t*

Pomocí makra přejmenujeme veškeré výskyty funkce accept na funkci acceptSocket, která bude vnitřně volat funkci accept s explicitním přetypováním na socklen_t*

Vytvoření konstant

V linuxu nejsou standartně konstanty SOCKET_ERROR a INVALID_SOCKET, stačí je přidat z winsock.h

Zdrojový kód

networking.h

#pragma once



#ifdef __unix__ /* __unix__ is usually defined by compilers targeting Unix systems */
//g++
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define closesocket(socket) close(socket)

typedef int SOCKET;
typedef unsigned short WORD;
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)

struct WSADATA {
 int x;
};


WORD MAKEWORD(int a, int b);

int WSAStartup(int a, WSADATA* b);
int WSACleanup();

#define accept(socket,addr,addrlen) acceptSocket(socket,addr,addrlen)
int acceptSocket(int s, struct sockaddr *addr, int* addrlen);

#elif defined _WIN32 /* _Win32 is usually defined by compilers targeting 32 or 64 bit Windows systems */
//WIN
#include <WinSock2.h>

#endif

networking.cpp

#include "networking.h"


#ifdef __unix__
//g++

WORD MAKEWORD(int a, int b) {
 return 0;
}

int WSAStartup(int a, WSADATA* b) {
 return 0;
}

int WSACleanup() {
 return 0;
}

#define acceptSocket(socket,addr,addrlen) accept(socket,addr,addrlen)

int acceptSocket(int s, struct sockaddr *addr, int* addrlen) {
 return accept(s, addr, (socklen_t*) addrlen);
}
#define accept(socket,addr,addrlen) acceptSocket(socket,addr,addrlen)

#endif

Závěr

Výše uvedený hlavičkový soubor umožňuje mým C++ projektům ve Visual Studiu 2010 zkompilovatelost v linuxu pomocí g++ bez nutnosti provádění změn. Jsem céčkař začátečník a tak nevím jestli neexistuje podobné lepší řešení (žádné jsem nenašel). Některé věci by určitě šli udělat líp a věrohodněji (aby WSA api opravdu něco dělalo, např. vracelo chybové kódy).

Zdroje

http://en.wikipedia.org/wiki/C_preprocessor
http://www.allegro.cc/forums/thread/370013/370226#target
http://tangentsoft.net/wskfaq/articles/bsd-compatibility.html
http://www.builder.cz/art/cpp/tcp_server_linux.html
http://www.builder.cz/art/cpp/tcp_server_windows.html
http://research.microsoft.com/en-us/um/redmond/projects/invisible/include/winsock.h.htm
http://pubs.opengroup.org/onlinepubs/7908799/xns/syssocket.h.html