Source file: /~heha/hsn/zqr.zip/src/zqr.cpp

#include "zqr.h"
#include <tchar.h>
#include <shlwapi.h>	// PathAppend
#include <shlobj.h>	// BROWSEINFO
#include <objbase.h>
#include <stdio.h>	// _snprintf
#include <Ks.h>
#include <Ksmedia.h>	// GUID: KSDATAFORMAT_SUBTYPE_PCM
#include <math.h>	// pow, log

CONFIG config;
int CONFIG::NIN=2;	// Anzahl Mixer-Eingänge

static TCHAR path[MAX_PATH];		// Pfad zu den MP3- und Vorbis-DLLs
       TCHAR file[MAX_PATH];		// Ton-Dateiname mit Tokens
static TCHAR note[MAX_PATH];		// Notizen-Dateiname

HINSTANCE hInstance;
HWND hMainWnd,hDialog,hSettingsDlg,hNotizDlg;

bool CONFIG::load() {
 HKEY key;
 bool ret=false;
 if (!RegOpenKeyEx(HKEY_CURRENT_USER,T("Software\\h#s"),0,KEY_READ,&key)) {
  TCHAR s[MAX_PATH];
  GetModuleFileName(hInstance,s,elemof(s));
  TCHAR*n=PathFindFileName(s);
  TCHAR*e=PathFindExtension(n); if (e!=n) *e=0;
  HKEY key2;
  if (!RegOpenKeyEx(key,n,0,KEY_READ,&key2)) {
   DWORD siz=sizeof config;
   BYTE block[sizeof config];
   if (!RegQueryValueEx(key2,T("config"),0,0,block,&siz)) {
    div_t d=div(int(siz-(sizeof config-sizeof config.q)),int(sizeof*config.q));
    if (1<=d.quot && d.quot<=MAXIN && !d.rem) {	// Länge im gültigen Bereich?
     config.NIN=d.quot;
     memcpy(&config,block,siz);
     ret=true;
    }					// sonst bleibt's bei der Vorgabe NCH=2
   }
   siz=sizeof file;
   RegQueryValueEx(key2,T("file"),0,0,(BYTE*)file,&siz);
   siz=sizeof path;
   RegQueryValueEx(key2,T("path"),0,0,(BYTE*)path,&siz);
   siz=sizeof note;
   RegQueryValueEx(key2,T("note"),0,0,(BYTE*)note,&siz);
   RegCloseKey(key2);
  }
  RegCloseKey(key);
 }
 return ret;
}

// Speichert unter HKCU\Software\h#s\<exename>,
// sodass der Anwender unter verschiedenen EXE-Dateinamen verschiedene Konfigurationen ablegen kann.
// Einerseits kann das durch mehrfache EXE-Dateikopien geschehen,
// unter NTFS eleganter mit symbolischen oder Hardlinks, wie unter Linux üblich
void CONFIG::save() {
 HKEY key;
 if (!RegCreateKeyEx(HKEY_CURRENT_USER,T("Software\\h#s"),0,0,0,KEY_WRITE,0,&key,0)) {
  TCHAR s[MAX_PATH];
  int l=LoadString(hInstance,108,s,elemof(s));		// "haftmann#software"
  RegSetValueEx(key,0,0,REG_SZ,(BYTE*)s,(l+1)*sizeof(TCHAR));	// Beschreibung für "h#s"
  GetModuleFileName(hInstance,s,elemof(s));
  TCHAR*n=PathFindFileName(s);
  TCHAR*e=PathFindExtension(n); if (e!=n) *e=0;
  HKEY key2;
  if (!RegCreateKeyEx(key,n,0,0,0,KEY_WRITE,0,&key2,0)) {
   l=GetWindowText(hMainWnd,s,elemof(s));		// "Zweiquellen-Rekorder" oder je nach Anzahl Quellen und Sprache
   RegSetValueEx(key2,0,0,REG_SZ,(BYTE*)s,(l+1)*sizeof(TCHAR));	// Beschreibung für <exename>
   RegSetValueEx(key2,T("config"),0,REG_BINARY,(BYTE*)&config,sizeof config-sizeof config.q+config.NIN*sizeof*config.q);
   RegSetValueEx(key2,T("file"),0,REG_SZ,(BYTE*)file,(lstrlen(file)+1)*sizeof(TCHAR));
   RegSetValueEx(key2,T("path"),0,REG_SZ,(BYTE*)path,(lstrlen(path)+1)*sizeof(TCHAR));
   RegSetValueEx(key2,T("note"),0,REG_SZ,(BYTE*)note,(lstrlen(note)+1)*sizeof(TCHAR));
   RegCloseKey(key2);
  }
  RegCloseKey(key);
 }
}

static NOTIFYICONDATA nid;
static TCHAR sDecimal[2];	// Punkt oder Komma
#define MBoxTitle nid.szTip

int vMBox(HWND Wnd, int id, UINT type, va_list va) {
 TCHAR fmt[256],s[512];
 LoadString(hInstance,id,fmt,elemof(fmt));
 _vsntprintf(s,elemof(s),fmt,va);
 return MessageBox(Wnd,s,MBoxTitle,type);
}

int _cdecl MBox(HWND Wnd, int id, UINT type, ...) {
 va_list va;
 va_start(va,type);
 int ret=vMBox(Wnd,id,type,va);
 va_end(va);
 return ret;
}

// Der Fehler-String in der Ressource muss %u %s oder %u %s %s enthalten!
void Error(HWND Wnd, int id, const TCHAR*s=0) {
 TCHAR*va[3];
 int i=0;
 DWORD e=GetLastError();
 va[i++]=(TCHAR*)(LONG_PTR)e;
 if (s) va[i++]=const_cast<TCHAR*>(s);
 FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_ALLOCATE_BUFFER,0,e,0,(TCHAR*)(va+i),0,0);
 vMBox(Wnd,id,MB_OK|MB_ICONEXCLAMATION,(va_list)va);
 LocalFree(va[i]);
}

inline void EnableDlgItem(HWND Wnd,int id,bool ena=true) {
 EnableWindow(GetDlgItem(Wnd,id),ena);
}
// für CBS_DROPDOWNLIST
static void FillCombo(HWND wnd,int id1,int id2,BYTE def) {
 TCHAR s[256];
 s[LoadString(hInstance,id2,s,elemof(s)-1)+1]=0;
 wnd=GetDlgItem(wnd,id1);
 ComboBox_ResetContent(wnd);
 for(const TCHAR*sp=s;*sp;sp+=lstrlen(sp)+1) {
  ComboBox_AddString(wnd,sp);
 }
 ComboBox_SetCurSel(wnd,def);
}
// für CBS_DROPDOWN
static void FillCombo2(HWND wnd,int id1,int id2,BYTE def) {
 TCHAR s[256];
 s[LoadString(hInstance,id2,s,elemof(s)-1)+1]=0;
 wnd=GetDlgItem(wnd,id1);
 ComboBox_ResetContent(wnd);
 for(const TCHAR*sp=s;*sp;sp+=lstrlen(sp)+1) {
  int i=ComboBox_AddString(wnd,sp);
  if (_ttoi(sp)==def) ComboBox_SetCurSel(wnd,i);
 }
 if (ComboBox_GetCurSel(wnd)<0) {
  _sntprintf(s,elemof(s),T("%u"),def);
  ComboBox_SetText(wnd,s);
 }
}
static void FillCombo2(HWND wnd,int id,BYTE def) { FillCombo2(wnd,id,id,def);}

static HIMAGELIST imagelist,	// 3 Bilder 21×21 Pixel: Lautsprecher, Mikrofon, Cinchstecker
	imagelist11;	// Raumklang-Symbolik

// für ComboBoxEx, hier für Raumklang
static void FillComboEx(HWND wnd,int id1,BYTE def) {
 wnd=GetDlgItem(wnd,id1);
 ComboBox_ResetContent(wnd);
 SendMessage(wnd,CBEM_SETIMAGELIST,0,(LPARAM)imagelist11);
 TCHAR s[256];
 LoadString(hInstance,109,s,elemof(s));
 COMBOBOXEXITEM cbei;
 cbei.mask=CBEIF_TEXT|CBEIF_IMAGE|CBEIF_SELECTEDIMAGE|CBEIF_LPARAM;
 for(cbei.iItem=0,cbei.pszText=s;cbei.iItem<MAXCH;cbei.pszText+=lstrlen(cbei.pszText)+1,cbei.iItem++) {
  cbei.lParam=LPARAM(cbei.iSelectedImage=cbei.iImage=int(cbei.iItem));
  SendMessage(wnd,CBEM_INSERTITEM,0,(LPARAM)&cbei);
 }
 ComboBox_SetCurSel(wnd,def);
}

inline int popcount(unsigned x) {	// Anzahl 1-Bits zählen (heutzutage=2020 Standard)
 int r=0; for(;x;x>>=1) r+=x&1; return r;
}

void registerhotkey(HWND Wnd,int id,WORD key=0) {	// Wnd ist nur für die Fehlermeldungen!
 UnregisterHotKey(hMainWnd,id);
 if (!key) return;
 BYTE modifier=HIBYTE(key);
 if (popcount(modifier)<2) MBox(Wnd,96,MB_OK|MB_ICONINFORMATION);
 else if (!RegisterHotKey(hMainWnd,id,modifier,LOBYTE(key))) Error(Wnd,97);
/*
  BYTE m=0;
  if (modifier&HOTKEYF_ALT)	m|=MOD_ALT;	// Bit 0
  if (modifier&HOTKEYF_CONTROL)	m|=MOD_CONTROL;
  if (modifier&HOTKEYF_SHIFT)	m|=MOD_SHIFT;
  if (modifier&HOTKEYF_EXT)	m|=MOD_WIN;	// Gleiche Bits??
*/
}

DWORD DSTCONFIG::getrate() const {
 switch (rate) {
  case 11: return 11025;
  case 22: return 22050;
  case 44: return 44100;
 }
 return rate*1000;
}

void setabsmax(int&a,int b) {b=abs(b); if (a<b) a=b;}

// Zahlenwert -120..120 = -20dB .. +20dB = Faktor 0,1 .. 10
// Die Gleitkommazahl wird als Festkommazahl mit 16 Nachkommabits ausgegeben
int gain2fak(char v) {
 return toInt(pow(10,float(v)/120)*65536);
}
// Eine 32-Bit-Festkommazahl mit 23 Nachkommabits in Lautstärkestufen umwandeln
// Da (mein) toInt() Schwierigkeiten mit -Inf hat wird -999 für 0 geliefert
inline double db(int f) {return f?log10(float(f)/0x7FFFFF)*20:-999;}
// und auf 0..100 begrenzen
BYTE fak2vol(int f) {
 int r=toInt(db(f)*100/config.z.dbumfang());	// 0.01 => -100
 if (r<-100) r=-100; else if (r>0) r=0;	// die 0 sollte nie überschritten werden
 return BYTE(r+100);
}

struct Statistik{	// Zur Berechnung von Spitzen- und Effektivwert (nur vorzeichenlos, nullzentriert)
 int n,maxi;
 __int64 sumq;
 Statistik():n(0),maxi(0),sumq(0) {}
 void operator<<(int v) {	// v = 23-Bit-Wert
  ++n;
  if (v<0) v=-v;
  if (maxi<v) maxi=v;
  sumq+=Int32x32To64(v,v);	// Mittels 32x32=64-Bit-Multiplikation quadrieren und summieren
 }
 int rms() const{return n?toInt(sqrt(double(sumq)/n)):0;}
 void reset() {memset(this,0,sizeof*this);}
};

struct VU{
 mutable BYTE pik[MAXCH];	// Spitzenwert links/rechts 0..100
 mutable BYTE rms[MAXCH];
 mutable bool ovl[MAXCH];	// Übersteuerung
 mutable int error;		// Fehlerkode (aus String-Resource) zur Anzeige bringen wenn !=0
 HWND hVU;			// VU-Meter
 WAVEFORMATEXTENSIBLE wf;	// nChannels wird benötigt, später der Platz für die GUID
 void initwf(DWORD rate,BYTE channels,BYTE bits) {InitWaveFormat(wf,rate,channels,bits);}
 mutable Statistik stat[MAXCH];
 bool enough_time() const{
  if (config.z.buftime()>=100) return true;
  DWORD toc=GetTickCount();
  DWORD age=toc-tic;
  if (age<100) return false;
  tic=toc; return true;
 }
private:
 mutable DWORD tic;		// Zeitpunkt des Beginns der Statistikauffüllung
};


static struct MIXER:public VU{
 int*buffer;	//Datenpuffer, 1..8 Kanäle, stets 24 Bit = FullScale
 unsigned lenS;	//Länge Datenpuffer in Mehrkanal-Samples
 unsigned use;	//Bitmaske der Füll-Teilnehmer, jedes Bit für 1 Quelle, bis 32 Quellen
 unsigned expect;	// Bitmaske der Füll-Teilnehmer
 CRITICAL_SECTION critsec;
 HWAVEOUT hwo;
 WAVEHDR wh[4];	// Arraylänge muss Zweierpotenz sein!
 unsigned whi;	// Wave-Header-Index
 unsigned initial;	// <initial> Blöcke aufheben, dann (mit dem 2. Block) absenden, um Lücken zu vermeiden
 bool allowput;
 void init(HWND h)	{hVU=h; InitializeCriticalSection(&critsec); reset(); allowput=false;}
 void reset()	{if (buffer) memset(buffer,0,lenS*config.z.getchan()*sizeof*buffer); use=0;}
// Was tun wenn von 1 Quelle 2 Blöcke kommen und von einer anderen keine?
 void realloc(unsigned new_len) {
  if (lenS==new_len) return;
  int*b=buffer;	// sicherheitshalber erst allozieren, dann den alten freigeben
  if (new_len) {
   lenS=new_len;
   buffer=new int[lenS*config.z.getchan()];
   reset();
  }else{
   lenS=0;
   buffer=0;
  }
  if (b) delete[] b;
 }
 unsigned realloc() {unsigned len=config.z.getrate()*config.z.buftime()/1000; realloc(len); return len;}
// Dann einfach überschreiben!
// Der Puffer wird auf echte 24 Bit limitiert, die Lautstärke ermittelt und Überläufe ermittelt
 void limit() {
  unsigned i,j,nch=config.z.getchan();
  memset(ovl,0,sizeof ovl);
  for (i=j=0;i<lenS*nch;i++,j++) {
   int sa=buffer[i];
   if (j==nch) j=0;
   if (sa<=-0x7FFFFE) {sa=-0x7FFFFE; ovl[j]=true;}
   if (sa>= 0x7FFFFF) {sa= 0x7FFFFF; ovl[j]=true;}
   stat[j]<<sa;
   buffer[i]=sa;
  }
  if (enough_time()) {
   for (j=0; j<nch; j++) {
    pik[j]=fak2vol(stat[j].maxi);
    rms[j]=fak2vol(stat[j].rms());
    stat[j].reset();
   }
   if (hVU) InvalidateRect(hVU,0,false);
  }
 }
 void putout(bool force=false) {
  if (force || use==expect) {	// Puffer von allen Quellen gefüllt?
   limit();			// Puffer auswerten
   if (allowput) put();
   if (save) {
    save->put(buffer,lenS);	// Puffer zum Speichern schicken (zurzeit im Workerthread-Kontext, aber im Gänsemarsch)
    PostMessage(hMainWnd,WM_USER+23,0,0);	// Zeitanzeige updaten
   }
   reset();			// Puffer leeren (besser: Neuen Puffer bereitstellen)
  }
 }
 bool connect() {		// TODO: Auf WASAPI umstellen, ist vielleicht schneller
  if (waveOutOpen(&hwo,config.z.sel,&wf.Format,(LONG_PTR)waveOutProc,(LONG_PTR)this,CALLBACK_FUNCTION)) return false;
  if (!lenS) realloc();
  unsigned bytelen=lenS*wf.Format.nBlockAlign;
  for (size_t i=0; i<elemof(wh); i++) {
   WAVEHDR&w=wh[i];
   w.lpData=new char[bytelen];	// insgesamt besteht 40 bzw. 400 ms Pufferzeit
   w.dwBufferLength=bytelen;
   w.dwUser=i;
   w.dwFlags=0;
   waveOutPrepareHeader(hwo,&w,sizeof w);
   w.dwFlags|=WHDR_DONE;
  }
  whi=0;
  initial=2;	// Einen WAVEHDR puffern = 100 ms hörbarer Versatz dazu (weitere 100..200 ms sind's ohnehin schon)
  allowput=true;
  return true;
 }
 void disconnect() {
  allowput=false;
  EnterCriticalSection(&critsec);	// Warten bis der Füller-Thread fertig ist
  waveOutReset(hwo);
  WAVEHDR*w;
  for (w=wh; w!=wh+elemof(wh); w++) {
   waveOutUnprepareHeader(hwo,w,sizeof*w);
  }
  waveOutClose(hwo);
  for (w=wh; w!=wh+elemof(wh); w++) {
   char*d=w->lpData;
   w->lpData=0;
   delete[] d;			// jetzt erst freigeben
  }
  LeaveCriticalSection(&critsec);
 }
 void put() {
  WAVEHDR&w=wh[whi];
  if (w.dwFlags&WHDR_DONE) {	// Frei zum Absenden?
   w.dwFlags&=~WHDR_DONE;
   convertWav(buffer,lenS*config.z.getchan(),w.lpData);
   if (initial && !--initial) for (WAVEHDR*wp=wh; wp!=&w; wp++) {
    waveOutWrite(hwo,wp,sizeof*wp);	// Gepufferte WAVEHDR ausspucken
   }				// sonst <initial-1> WAVEHDR puffern = aufheben und nichts tun
   if (!initial) waveOutWrite(hwo,&w,sizeof w);	// im Normalfall den einen Puffer sofort ausschreiben
   whi=(whi+1)&(elemof(wh)-1);
  }
 }
 static void CALLBACK waveOutProc(HWAVEOUT,UINT msg,const MIXER&mixer,WAVEHDR&wh,void*) {	// (Achtung Multithreading)
  if (msg!=WOM_DONE) return;
// Hier ließe sich nichts weiter machen als die Anzahl benutzter/freier Puffer anzuzeigen
 }
}mixer;


// Mehrfach-Recorder, daher NIN Instanzen (Speicher für MAXIN)
static struct REC:public VU{
 WAVEHDR wh[4];
 HWAVEIN h;
 wasapi::RECORD*wasapiRecord;	// Zeiger auf WASAPI-Objekt, um COM aus zqr.cpp herauszuhalten (würde Compilefehler produzieren)
 static void wasapiInProc(const char*,DWORD,void*);		// Habe-Datenpaket-Callback für Wasapi-Thread (Achtung Multithreading)
 void init(HWND=0,DWORD rate=0,BYTE channels=0,BYTE bits=0);	// Was !=0 ist wird initialisiert
 bool connect(char);
 bool connect();
 void disconnect();
 bool alive();	// zyklischer Aufruf vom Hauptfenster
 static void CALLBACK waveInProc(HWAVEIN,UINT,const REC&,WAVEHDR&,void*);	// (Achtung Multithreading)
 void gotWave(const char*,DWORD) const;						// (Achtung Multithreading)
 int (*getSample)(const char*);		// Rechengröße: 24 Bit (-8M .. 8M)
 int maxvalue;		// 0x7F0000 für 8 Bit, 0x7FFF00 für 16 Bit, 0x7FFFFF für 24 Bit
 static int regelval;		// Zielwert bei AGC mit quellenanzahlabhängigem Wert
 HWND hGain;
// mutable int samax[2];		// Maximale Amplitude eines Mixer-Datenblocks (links/rechts oder Mono)
// Basis sind 24-Bit-Samples in 32-Bit-Containern. Das läuft nicht so schnell über.
 static int getByteSample(const char*p) {return (*(BYTE*)p-128)<<16;}
 static int getInt16Sample(const char*p) {return *(short*)p<<8;}
 static int getInt24Sample(const char*p) {return *(int*)p<<8>>8;}	// Naja: Lesezugriff 1 Byte außerhalb Array-Grenze möglich
 static int getInt32Sample(const char*p) {return *(int*)p>>8;}
}rec[MAXIN];

int REC::regelval;		// Zielwert (gleich für alle Kanäle) für AGC

void InitWaveFormat(WAVEFORMATEXTENSIBLE&wf,DWORD rate,BYTE channels,BYTE bits) {
 if (channels) {
  wf.Format.nChannels=channels;
  wf.Format.wFormatTag=channels>2?WAVE_FORMAT_EXTENSIBLE:WAVE_FORMAT_PCM;	// Windows besteht auf die komische GUID für nChannels>2
 }
 if (bits) {
  wf.Format.wBitsPerSample=bits;
 }
 if (rate) wf.Format.nSamplesPerSec=rate;
 wf.Format.nBlockAlign=wf.Format.nChannels*((wf.Format.wBitsPerSample+7)>>3);
 wf.Format.nAvgBytesPerSec=wf.Format.nSamplesPerSec*wf.Format.nBlockAlign;
 if (wf.Format.nChannels>2) {
  wf.Format.cbSize=22;
  wf.Samples.wValidBitsPerSample=wf.Format.wBitsPerSample;
  static const DWORD speakers[6]={0x7,0x33,0x37,0x3F,0x13F,0x1013F};
  wf.dwChannelMask=speakers[wf.Format.nChannels-3];
  wf.SubFormat=KSDATAFORMAT_SUBTYPE_PCM;
 }else{
  wf.Format.cbSize=0;
 }
}

void REC::init(HWND hvu,DWORD rate,BYTE channels,BYTE bits) {
 if (hvu) hVU=hvu;
 InitWaveFormat(wf,rate,channels,bits);
 if (bits) {
  getSample=bits>24?getInt32Sample:bits>16?getInt24Sample:bits>8?getInt16Sample:getByteSample;
  if (bits>24) bits=24;
  maxvalue=((1<<(bits-1))-1)<<(24-bits);	// 0x7F0000 oder 0x7FFF00 oder 0x7FFFFF, das was getIntXXSample() maximal liefern kann (24 Bit vzb.)
 }
}

bool REC::connect(char devid) {
 DWORD bufferlen=mixer.realloc();	// in Blocksamples für 100 ms, ohne ×2 für Stereo
 if (config.z.wasapi()) {
  wasapiRecord=new wasapi::RECORD(devid,&wf,bufferlen,wasapiInProc,this);
  bool ret=wasapiRecord->start();
  if (ret) mixer.expect|=1U<<unsigned(this-rec);
  return ret;
 }
 for(MMRESULT mmr;mmr=waveInOpen(&h,devid,&wf.Format,(LONG_PTR)waveInProc,(LONG_PTR)this,CALLBACK_FUNCTION);) {
  if (mmr!=WAVERR_BADFORMAT) return false;
// Kanäle herunterschrauben (Test??)
  if (wf.Format.nChannels==1) return false;	// Mono müsste immer gehen!
  init(0,0,wf.Format.nChannels-1);
  int idx=int(this-rec);
  config.q[idx].setchan((BYTE)wf.Format.nChannels);
  SendDlgItemMessage(hMainWnd,MAKELONG(30,idx),CB_SETCURSEL,wf.Format.nChannels-1,0);
 }
 bufferlen*=wf.Format.nBlockAlign;	// Zehntelsekunde Blocklänge = Reaktionszeit VU-Meter
 for (size_t i=0; i<elemof(wh); i++) {
  WAVEHDR&w=wh[i];
  w.lpData=new char[bufferlen];
  w.dwBufferLength=bufferlen;
  w.dwUser=i;
  w.dwFlags=0;
  waveInPrepareHeader(h,&w,sizeof w);
  waveInAddBuffer(h,&w,sizeof w);
 }
 mixer.expect|=1U<<unsigned(this-rec);
 waveInStart(h);
 return true;
}

bool REC::connect() {return connect(config.q[this-rec].sel);}	// automatisch aus globaler Konfiguration

bool REC::alive() {
 bool r;
 if (wasapiRecord) {	// mit WASAPI
  r=wasapiRecord->alive();
 }else r=!!h;		// mit mmsystem (da wäre gesondert zu testen)
 int err=r?0:110;	// "cannot grab audio"
 if (error!=111 && error!=err) {
  error=err;
  InvalidateRect(hVU,0,false);	// Bei Fehleränderung aktualisieren lassen
 }
 return r;
}

void REC::disconnect() {
 mixer.expect&=~(1U<<unsigned(this-rec));	// Diesen Aufnahmekanal beim Mixer abmelden
 if (wasapiRecord) {
  wasapiRecord->stop();
  delete wasapiRecord;
  wasapiRecord=0;
  return;
 }
 if (!h) return;
 waveInStop(h);
 WAVEHDR*w;
 for (w=wh; w!=wh+elemof(wh); w++) {
  waveInUnprepareHeader(h,w,sizeof*w);
 }
 waveInClose(h);
 for (w=wh; w!=wh+elemof(wh); w++) {
  char*d=w->lpData;
  w->lpData=0;		// Zeiger ungültig machen
  delete[] d;		// threadsicher in dieser Reihenfolge
 }
}

/*=========================================================================================*
 * Achtung! WAVEIN-Datenverarbeitung (die folgenden 3 Memberfunktionen) in Worker-Threads! *
 *=========================================================================================*/
void REC::wasapiInProc(const char*data,DWORD len,void*cbd) {	// Mittlerweile kommen die Daten nur noch in Wunschlänge = ganzer Mixer-Puffer
 REC*rec=((REC*)cbd);
 rec->gotWave(data,len*rec->wf.Format.nBlockAlign);	// schickt den vollen mixer.buffer „hinaus“
}

void REC::waveInProc(HWAVEIN h,UINT msg,const REC&rec,WAVEHDR&wh,void*) {
 if (msg!=WIM_DATA) return;
 rec.gotWave(wh.lpData,wh.dwBytesRecorded);	// dwBytesRecorded passt im Normalfall genau auf die MixBuffer-Größe
 waveInAddBuffer(h,&wh,sizeof wh);
}

void REC::gotWave(const char*lpData,DWORD dwBytesRecorded) const{
 error=lpData?0:111;
 const char*e=lpData+dwBytesRecorded;
 int idx=int(this-rec);
 DWORD mask=1<<idx;
 EnterCriticalSection(&mixer.critsec);
 if (mixer.use&mask) {
  debugmsg(T("zqr:overlap: mixer.use=%u\n"),mixer.use);
  mixer.putout(true);	// Nicht von allen Teilnehmern („halb“) befüllten Puffer trotzdem ausgeben
 }
 ovl[0]=ovl[1]=false;	// links/rechts
 int*mix=mixer.buffer;
 SRCCONFIG&q=config.q[idx];
 int fakL=gain2fak(q.gain);	// Wertebereich 6K4..640K, exponenziell
 int samaxAll=0;	// Maximum-Sample über alle Kanäle (für AGC)
// TODO: Für alle Kombinationen aus Quell- und Zielkanalzahlen auslegen
// Quelle/Ziel	1	2	3	4	5	6
//	1	1:1	Pano	PanoC	Pano	PanoC	PanoC		Pano = Panorama Links/Rechts, PanoC = Panorama Links/Center/Rechts
//	2	mix	1:1	Z:0	Z:0	Z:0	Z:0		Z:0 = Zusätzliche Kanäle Null
//	3	mix	C->LR	1:1	C->LR	Z:0	Z:0		C->LR = Center auf links + rechts addieren
//	4	mix	m	m	1:1	C:0	C,S:0		m = hinten nach vorn seitenrichtig addieren, C:0 = kein Center, S:0 = kein Subwoofer
//	5	mix	m,C->LR	m	C->LR	1:1	S:0
//	6	mix	m,C->LR	m	C->LR		1:1		Der Subwoofer wird auf C oder hälftig auf L+R addiert
 unsigned nch=wf.Format.nChannels;
 bool enough=enough_time();
 if (nch==2) {
//  int samax[2]={0,0};
  unsigned bpc=wf.Format.nBlockAlign>>1;
  for(const char*p=lpData;p!=e;p+=wf.Format.nBlockAlign) {
   int sal=getSample(p);
   int sar=getSample(p+bpc);
   stat[0]<<sal;
   stat[1]<<sar;
   sal=int(Int32x32To64(sal,fakL)>>16);
   sar=int(Int32x32To64(sar,fakL)>>16);
   if (config.z.getchan()==2) {	// Stereo-Senke
    *mix+++=sal;		// TODO: Wäre "*mix+=" ein atomarer Befehl könnte man sich hier den hässlichen kritischen Abschnitt sparen!
    *mix+++=sar;		// Andererseits ist beim Mixer gemeinsamer Speicher logisch und über den L1-Cache bremsend bei gleichzeitiger Nutzung.
   }else{
    *mix+++=(sal+sar)>>1;	// Mono = Summensignal
   }
  }
  if (enough) for (int ch=0; ch<2; ch++) {
   pik[ch]=fak2vol(stat[ch].maxi);	// logarithmisch 0..100 auf -40..0 dB
   rms[ch]=fak2vol(stat[ch].rms());
   if (stat[ch].maxi>=maxvalue) ovl[ch]=true;
   if (samaxAll<stat[ch].maxi) samaxAll=stat[ch].maxi;
   stat[ch].reset();
  }
 }else{
  int pano=(config.q[idx].panorama<<16)/100;	// Wertebereich -64K .. 64K
  for(const char*p=lpData;p!=e;p+=wf.Format.nBlockAlign) {
   int sa=getSample(p);
   stat[0]<<sa;
//   setabsmax(samaxAll,sa);
   sa=int(Int32x32To64(sa,fakL)>>16);
   if (config.z.getchan()==2) {	// Stereo-Senke
// TODO: Bei den Zielkanalzahlen 3, 5 und 6 sollte das Monosignal zum Center-Lautsprecher zugeordnet werden statt anteilig links/rechts!
    *mix+++=int(Int32x32To64(sa,65536-pano)>>17);	// Bei pano = -100 voll nach links
    *mix+++=int(Int32x32To64(sa,65536+pano)>>17);	// Bei pano = +100 voll nach rechts
   }else{
    *mix+++=sa;
   }
  }
  if (enough) {
   samaxAll=stat[0].maxi;
   pik[0]=fak2vol(samaxAll);	// logarithmisch 0..100 auf -40..0 dB
   rms[0]=fak2vol(stat[0].rms());
   if (samaxAll>=maxvalue) ovl[0]=true;
   stat[0].reset();
  }
 }
 mixer.use|=mask;
 mixer.putout();		// Mglw. sinnvolle Idee: Ausgabe per PostMessage serialisieren statt alles im Workerthread (mglw. blockierend) zu tun.
 LeaveCriticalSection(&mixer.critsec);
 if (enough && hVU) InvalidateRect(hVU,0,false);
 if (enough && q.getAGC()) {
  samaxAll=int(Int32x32To64(samaxAll,fakL)>>16);	// Das Maximum (welches noch unmultipliziert ist) mit Lautstärke skalieren
  int v=q.gain;
  if (samaxAll>regelval) {	// schnell aber nicht schlagartig abregeln
   v+=(regelval-samaxAll)>>18;	// TODO: Um Knackgeräusche zu vermindern müsste der Faktor stetig-gleitend sein
  }else{
   v+=(regelval-samaxAll)>>20;	// langsam aufregeln, abhängig von der Differenz zur Zielvorgabe „Vollaussteuerung“
  }
  if (v<-120) v=-120;
  else if (v>120) v=120;
  if (q.gain!=v) {
   q.gain=char(v);
   PostMessage(hGain,TBM_SETPOS,1,v);
  }
 }
}
/*== Ende Workerthread-Kontext ==*/

bool dllLoad(HINSTANCE&hLib,const TCHAR*name) {
 if (hLib && FreeLibrary(hLib)) hLib=0;
 if (!name) return true;
 TCHAR dll[MAX_PATH];
 lstrcpyn(dll,path,elemof(dll));
 PathAppend(dll,name);
 hLib=LoadLibrary(dll);
 if (!hLib) hLib=LoadLibrary(name);
 if (!hLib) {
#ifdef _DEBUG
  Error(hMainWnd,99,dll);
#endif
  return false;
 }
 return true;
}

// Gruppe aus Hotkey-Editor und Win-Modifizier-Checkbox setzen
void sethotkey(HWND Wnd, int id, WORD hotkey) {
 HWND w=GetDlgItem(Wnd,id);
 SendMessage(w,HKM_SETRULES,HKCOMB_NONE,HOTKEYF_ALT);
 SendMessage(w,HKM_SETHOTKEY,hotkey&~0x800,0);
 CheckDlgButton(Wnd,id+1,hotkey>>11);
}

WORD gethotkey(HWND Wnd, int id) {
 return WORD(SendDlgItemMessage(Wnd,id,HKM_GETHOTKEY,0,0)
 |IsDlgButtonChecked(Wnd,id+1)<<11);
}

// Aufruf nur erlaubt wenn keine Aufnahme läuft!
void setoutputsel(BYTE sel=0xFF) {
 if (sel>=3) sel=config.z.getfileformat();
 else{
  if (config.z.getfileformat()==sel) return;
  TCHAR*p=PathFindExtension(file);
  if (p && *p=='.') {
   ++p;
   lstrcpyn(p,TEXT("wav\0mp3\0ogg")+(sel<<2),int(file+elemof(file)-p));
   SetDlgItemText(hMainWnd,104,file);
  }
 }
 config.z.setfileformat(sel);
 CheckRadioButton(hMainWnd,121,123,121+sel);	// WAV/MP3/Vorbis
 HWND w=GetDlgItem(hMainWnd,102);	// Bits
 EnableWindow(w,!sel);			// Bits wählbar nur bei WAV
 if (sel) {
  config.z.setbits(16);
  ComboBox_SetCurSel(w,1);	// Nur 16 Bits bei MP3 und Vorbis
 }
 w=GetDlgItem(hMainWnd,124);	// Qualität (Bitrate)
 EnableWindow(w,!!sel);		// wählbar nur bei MP3 und Vorbis
}

void enableoutputsel(bool e=true) {
 for (int i=0; i<3; i++) {
  bool en=saveobj[i] && saveobj[i]->okay();
  EnableDlgItem(hMainWnd,121+i,e&en);
  if (!en && config.z.getfileformat()==i) setoutputsel(0);
 }
}

static void setWindowTitle() {
 TCHAR s[128],t[128],*p=t;
 LoadString(hInstance,107,t,elemof(t));
 for(int k=0; k<config.NIN; k++) p+=lstrlen(p)+1;
 _sntprintf(s,elemof(s),t,p);
 TCHAR*u=s;
 if (config.z.recording()) {
  _sntprintf(t,elemof(t),TEXT("%s - %s"),PathFindFileName(currentfile),s);
  u=t;
 }
 SetWindowText(hMainWnd,u);
 lstrcpyn(nid.szTip,u,sizeof nid.szTip);
 Shell_NotifyIcon(NIM_MODIFY,&nid);
}

static void recordchange() {
 bool en=config.z.recording();
 EnableDlgItem(hMainWnd,120,en||currentfile[0]);	// Notiz (kann auch nach der Aufnahme angehangen werden)
 ShowWindow(GetDlgItem(hMainWnd,118),en?SW_SHOW:SW_HIDE);	// Zeitanzeige
 nid.hIcon=LoadIcon(hInstance,MAKEINTRESOURCE(100+en));	// Icon (rote LED)
 setWindowTitle();
// Shell_NotifyIcon(NIM_MODIFY,&nid);
 SetClassLongPtr(hMainWnd,GCLP_HICON,(LONG_PTR)nid.hIcon);
 SetClassLongPtr(hMainWnd,GCLP_HICONSM,(LONG_PTR)nid.hIcon);
 en=!en;
 EnableDlgItem(hMainWnd,101,en);			// Rate
 EnableDlgItem(hMainWnd,102,en && !config.z.getfileformat());	// Bits (bei MP3 und Vorbis stets 16)
 EnableDlgItem(hMainWnd,103,en);			// Stereo
 EnableDlgItem(hMainWnd,124,en && !!config.z.getfileformat());	// Qualität (nur bei MP3 und Vorbis)
 enableoutputsel(en);					// Auswahl Kompressionsformat
 EnableDlgItem(hMainWnd,104,en);			// Dateiname
 EnableDlgItem(hMainWnd,105,en);			// Dateiauswahl
}

static INT CALLBACK BrowseCallbackProc(HWND Wnd, UINT msg, LPARAM lp, LPARAM pData) {
 if (msg==BFFM_INITIALIZED) SendMessage(Wnd,BFFM_SETSELECTION,TRUE,pData);
 return 0;
}

static void FillWaveinCombos() {
 HWND h[MAXIN];
 char sel[MAXIN];
 int idx,i;
 for (idx=0; idx<config.NIN; idx++) {
  h[idx]=GetDlgItem(hMainWnd,MAKEWORD(10,idx));
  ComboBox_ResetContent(h[idx]);
  SendMessage(h[idx],CBEM_SETIMAGELIST,0,(LPARAM)imagelist);
  sel[idx]=config.q[idx].sel;
 }
 COMBOBOXEXITEM cbei;
 cbei.mask=CBEIF_TEXT|CBEIF_IMAGE|CBEIF_SELECTEDIMAGE|CBEIF_LPARAM;
 cbei.iItem=-1;				// = anhängen
 if (config.z.wasapi() && wasapi::FillCombos(h,sel,&cbei)) return;
 config.z.wasapi(false);	// Kein WASAPI
// cbei.iImage=cbei.iSelectedImage=1;	// Mikrofon
 int n=waveInGetNumDevs();
 WAVEINCAPS wic;
 struct CFC{HWND*h;char*sel;COMBOBOXEXITEM*cbei;}cfc={h,sel,&cbei};
 for (i=WAVE_MAPPER; i<n; i++) {
  if (!waveInGetDevCaps(i,&wic,sizeof wic)) {
   ComboFillCallback((UINT)i,true,wic.szPname,&cfc);
  }
 }
}

static void FillWaveoutCombo(HWND hCombo) {
 ComboBox_ResetContent(hCombo);
 SendMessage(hCombo,CBEM_SETIMAGELIST,0,(LPARAM)imagelist);
 COMBOBOXEXITEM cbei;
 cbei.mask=CBEIF_TEXT|CBEIF_IMAGE|CBEIF_SELECTEDIMAGE|CBEIF_LPARAM;
 cbei.iItem=-1;				// = anhängen
 cbei.iImage=cbei.iSelectedImage=0;	// Lautsprecher
 int n=waveOutGetNumDevs();
 WAVEOUTCAPS woc;
 cbei.pszText=woc.szPname;
 for (int i=WAVE_MAPPER; i<n; i++) {
  if (!waveOutGetDevCaps(i,&woc,sizeof woc)) {
   cbei.lParam=(LPARAM)i;
   int k=int(SendMessage(hCombo,CBEM_INSERTITEM,0,(LPARAM)&cbei));
   if (i==config.z.sel) ComboBox_SetCurSel(hCombo,k);	// bei Übereinstimmung mit <sel>
  }
 }
}

struct CLONEINFO{
 int left,top,width,height;
 DWORD style,exstyle;
 ATOM wndclass;
 UINT id;
 HFONT font;
 LPVOID lParam;
 int idx;
 TCHAR text[128];
};

static bool CloneWindowsCallback(CLONEINFO&ci,void*cbd) {
 int dx=LOWORD((LPARAM)cbd);
 int j=HIWORD((LPARAM)cbd);
 ci.left+=j*dx;	// Position
 ci.id+=j<<8;	// ID
 switch (ci.idx) {
  case 0: {
   TCHAR*f=_tcschr(ci.text,'&');
   if (f) {
    if (j<9) ++f;		// "&" erhalten, höhere Kanalnummern ohne Tasten-Hotkey
    _sntprintf(f,ci.text+elemof(ci.text)-f,TEXT("%u"),j+1);	// Kanalnummer 1-basiert
   }
  }break;
  case 1:
  case 3:
  case 4: {
   ci.height=200;	// Höhenfehler beim Klonen von Comboboxen ausmerzen
  }break;
 }
 return ci.idx<8;
}

// Kindfenster in Dialogen klonen, ab <w>, einsetzen nach <after>.
// Die Anzahl ergibt sich aus der Rückgabe von <cb>, true für weiteres Fenster.
// Beim Aufruf von <cb> enthält CLONEINFO& folgende manipulierbare Daten:
// * ci.left, ci.top = Position links-oben relativ zum Parent
// * ci.width, ci.height = Breite bzw. Höhe der Vorlage. Achtung: Höhe falsch bei Comboboxen!
// * ci.style und ci.exstyle die Fensterstilbits
// * ci.wndclass = Fensterklasse
// * ci.id = Fenster-ID
// * ci.font = Fenster-Schriftart
// * ci.text = Fenstertext
// * ci.lParam = zusätzlicher Create-Parameter
// Der Callback muss zumindest die Position ändern. <cbd> wird durchgereicht.
// * ci.idx sollte nicht geändert werden!
static HWND clonewindows(HWND w,HWND after,bool(*cb)(CLONEINFO&,void*),void*cbd) {
 HWND wn=w;
 HWND p=GetParent(w);
 CLONEINFO ci;
 WINDOWINFO wi;
 wi.cbSize=sizeof wi;
 for(ci.idx=0;w;ci.idx++,w=GetNextSibling(w)) {
  GetWindowInfo(w,&wi);
  ci.width=wi.rcWindow.right-wi.rcWindow.left;
  ci.height=wi.rcWindow.bottom-wi.rcWindow.top;
  ci.left=wi.rcWindow.left;
  ci.top=wi.rcWindow.top;
  ScreenToClient(p,(POINT*)&ci.left);
  ci.style=wi.dwStyle;
  ci.exstyle=wi.dwExStyle&0x3FFFFFFF;	// Bei Gerald kommen die oberen 2 Bits gesetzt, und CreateWindowEx() versagt!
  ci.wndclass=wi.atomWindowType;
  ci.font=GetWindowFont(w);
  ci.id=GetWindowID(w);
  ci.lParam=0;
  GetWindowText(w,ci.text,elemof(ci.text));
  bool cont=cb(ci,cbd);
  wn=CreateWindowEx(ci.exstyle,MAKEINTATOM(ci.wndclass),ci.text,ci.style,
  ci.left,ci.top,ci.width,ci.height,
  p,(HMENU)(LONG_PTR)ci.id,0,ci.lParam);
 #ifdef _DEBUG	// der Compiler wirft sonst den unbenutzten String nicht heraus, hm, Problem??
  if (!wn) {
   debugmsg(T("CreateWindowEx(%X,%X,%s,%X, %d,%d,%d,%d, %X,%d,0,%X) failed\n"),
   ci.exstyle,ci.wndclass,ci.text,ci.style,
   ci.left,ci.top,ci.width,ci.height,
   p,ci.id,ci.lParam);
  }
 #endif
  SetWindowFont(wn,ci.font,false);
  SetWindowPos(wn,after,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE);
  after=wn;
  if (!cont) break;
 }
 return after;
}

static void restorePos(HWND Wnd,const POINTS&pos) {
 if (!pos.x && !pos.y) return;	// sieht so aus als wär's uninitialisiert: Nichts tun! Besser: Konstruktor mit INAN
 WINDOWPLACEMENT wp;
 wp.length=sizeof wp;
 GetWindowPlacement(Wnd,&wp);
 OffsetRect(&wp.rcNormalPosition,pos.x-wp.rcNormalPosition.left,pos.y-wp.rcNormalPosition.top);
 SetWindowPlacement(Wnd,&wp);	// SetWindowPlacement sollte gegenüber SetWindowPos die Monitorausdehnung beachten und das Fenster keinesfalls unsichtbar außerhalb platzieren
//    SetWindowPos(Wnd,0,config.posx,config.posy,0,0,SWP_NOSIZE|SWP_NOZORDER);
}

static void savePos(HWND Wnd,POINTS&pos) {
 WINDOWPLACEMENT wp;
 wp.length=sizeof wp;
 GetWindowPlacement(Wnd,&wp);
 pos.x=(short)wp.rcNormalPosition.left;
 pos.y=(short)wp.rcNormalPosition.top;
}

static int mainWndWidth;

static void onNinChange() {
 int i;
 for (i=MAXIN; --i;) {
  HWND w=GetDlgItem(hMainWnd,MAKEWORD(10,i));
  if (w) {
   w=GetPrevSibling(w);
   int j=8;
   do{
    HWND wn=GetNextSibling(w);
    DestroyWindow(w);
    w=wn;
   }while(--j);
  }
 }
 RECT rc;
 GetWindowRect(GetFirstChild(hMainWnd),&rc);
 const int dx=rc.right-rc.left+6;	// 12 Pixel Luft
 for (i=0; i<config.NIN; i++) {
  TCHAR r[3]=T("&1");
  r[1]+=i;
  HWND wndafter=i?clonewindows(GetFirstChild(hMainWnd),wndafter,CloneWindowsCallback,(void*)(LPARAM)MAKELONG(dx,i)):GetDlgItem(hMainWnd,60);
 }
 GetWindowRect(hMainWnd,&rc);
 rc.bottom-=rc.top;	// Höhe bleibt
 rc.right=mainWndWidth+(config.NIN>2?(config.NIN-2)*dx:0);
 SetWindowPos(hMainWnd,0,0,0,rc.right,rc.bottom,SWP_NOMOVE|SWP_NOZORDER);	// Fensterbreite anpassen
 setWindowTitle();
}

//static HWND hTooltip;

static void onNinChange(HWND Wnd) {
 for (int i=0; i<config.NIN; i++) {
  SRCCONFIG&q=config.q[i];
  HWND w=GetDlgItem(Wnd,MAKEWORD(50,i));
  CheckDlgButton(Wnd,MAKEWORD(40,i),q.getAGC());
  EnableWindow(w,!q.getAGC());
  SendMessage(w,TBM_SETRANGE,0,MAKELONG(-120,120));
  SendMessage(w,TBM_SETTICFREQ,30,0);
  SendMessage(w,TBM_SETPAGESIZE,0,30);
  SendMessage(w,TBM_SETPOS,1,q.gain);
//  SendMessage(w,TBM_SETTOOLTIPS,(WPARAM)hTooltip,0);
  rec[i].hGain=w;	// Zielfenster bei aktivem AGC

  w=GetDlgItem(Wnd,MAKEWORD(60,i));
  EnableWindow(w,q.getchan()<=1 && config.z.getchan()>=2);	// Panoramaregler NUR bei Mono-Quelle und Stereo-Senke
  SendMessage(w,TBM_SETRANGE,0,MAKELONG(-100,100));
  SendMessage(w,TBM_SETTICFREQ,20,0);
  SendMessage(w,TBM_SETPAGESIZE,0,20);
  SendMessage(w,TBM_SETPOS,1,q.panorama);

  FillCombo(Wnd,MAKEWORD(70,i),102,q.getbytes()-1);
  FillComboEx(Wnd,MAKEWORD(30,i),q.getspat());
 }
 static const BYTE regelfak[MAXIN-1]={80,50,30,25,22,21,20};	// Index 0 für NIN==1
 REC::regelval=MulDiv(0x7F7F7F,regelfak[config.NIN-1],100);
}

static void CloseOpenAllWaveIn(DWORD rate=0,int nin=0) {
 REC*rp;
 for (rp=rec; rp!=rec+config.NIN; rp++) rp->disconnect();	// erst alle trennen
 if (config.z.echoing()) mixer.disconnect();
 if (nin) config.NIN=nin;		// neuzuweisen im Falle der Kanalzahländerung
 if (rate) mixer.initwf(rate,0,0);
 if (config.z.echoing() && !mixer.connect()) {
  config.z.echoing(false);
  CheckDlgButton(hMainWnd,126,0);
 }
 if (rate) for (rp=rec; rp<rec+config.NIN; rp++) rp->init(0,rate);
 for (rp=rec; rp!=rec+config.NIN; rp++) rp->connect();
}

static HFONT hfntAlias;		// Kleine Schrift ohne AntiAlias für Skalenbeschriftung
static HBITMAP hbmVuXor[2];	// Vorgefertigte Bitmaps für Aussteuerungsanzeigen (verschiedene Größen)

INT_PTR CALLBACK SettingsDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
 switch (Msg) {
  case WM_INITDIALOG: {
   HFONT f=GetWindowFont(Wnd);
   LOGFONT lf;
   GetObject(f,sizeof lf,&lf);
   lf.lfHeight=MulDiv(lf.lfHeight,14,9);
   lf.lfCharSet=SYMBOL_CHARSET;
   lstrcpyn(lf.lfFaceName,TEXT("Wingdings"),sizeof lf.lfFaceName);
   HFONT f2=CreateFontIndirect(&lf);
   SendDlgItemMessage(Wnd,11,WM_SETFONT,(WPARAM)f2,0);
   SendDlgItemMessage(Wnd,13,WM_SETFONT,(WPARAM)f2,0);
   restorePos(Wnd,config.posSettings);
   sethotkey(Wnd,10,config.hotkey[0]);
   sethotkey(Wnd,12,config.hotkey[1]);
   SetDlgItemText(Wnd,20,path);
   SetDlgItemText(Wnd,22,note);
   CheckDlgButton(Wnd,24,config.z.wasapi());
   CheckDlgButton(Wnd,27,config.z.flags>>4&1);
   CheckDlgButton(Wnd,28,config.z.flags>>5&1);
   CreateUpDownControl(
     WS_CHILD|WS_BORDER|WS_VISIBLE|UDS_ALIGNRIGHT|UDS_ARROWKEYS|UDS_HOTTRACK|UDS_SETBUDDYINT,
     0,0,0,0,Wnd,26,0,GetDlgItem(Wnd,25),MAXIN,1,config.NIN);
  }return TRUE;

  case WM_MOVE: savePos(Wnd,config.posSettings); break;

  case WM_ACTIVATE: if (wParam) hDialog=Wnd; break;

  case WM_COMMAND: switch (wParam) {
   case 5:	// Übernehmen
   case 1:{	// OK
    registerhotkey(Wnd,1,config.hotkey[0]=gethotkey(Wnd,10));
    registerhotkey(Wnd,120,config.hotkey[1]=gethotkey(Wnd,12));
    GetDlgItemText(Wnd,20,path,elemof(path));
    GetDlgItemText(Wnd,22,note,elemof(note));
    BYTE flags=0;
    if (IsDlgButtonChecked(Wnd,24)) flags|=0x02;
    if (IsDlgButtonChecked(Wnd,27)) flags|=0x10;
    if (IsDlgButtonChecked(Wnd,28)) flags|=0x20;
    int nin=GetDlgItemInt(Wnd,25,0,0);
    if (config.NIN!=nin) {
     CloseOpenAllWaveIn(0,nin);	// Alle WaveIn-Devices schließen und öffnen
     onNinChange();		// Teil 1: Klonen
     onNinChange(hMainWnd);	// Teil 2: Füllen
     FillWaveinCombos();
    }
    BYTE change=flags^config.z.flags;
    config.z.flags = config.z.flags&0xCD|flags;	// Neue Flags setzen
    if (change&0x02) {
     FillWaveinCombos();
     if ((flags^config.z.flags)&0x02) {	// Kann von FillWaveinCombos nur von 1 auf 0 fallen
      CheckDlgButton(Wnd,24,0);		// Beim „Übernehmen“ anzeigen, dass das nicht klappt (Windows XP)
      change&=~0x02;
     }
    }
    if (change&0x12) {
     CloseOpenAllWaveIn();     // TODO: Reicht das für die Latenzumschaltung?
    }
    if (change&0x20) {		// dB-Umschaltung
     DeleteBitmap(hbmVuXor[0]); hbmVuXor[0]=0;
     DeleteBitmap(hbmVuXor[1]); hbmVuXor[1]=0;
    }
    if (!config.z.recording()) {
     SAVE::deleteall();
     SAVE::newall();
     enableoutputsel();
    }
    if (wParam!=1) break;	// kein DestroyWindow
   }
   case 2: {
    DestroyWindow(Wnd); 
   }break;
   case 21: {	// Verzeichniswahl
    HWND hEdit=GetPrevSibling((HWND)lParam);
    TCHAR p[MAX_PATH];
    GetWindowText(hEdit,p,elemof(p));
    BROWSEINFO br;
    ZeroMemory(&br,sizeof br);
    br.lpfn=BrowseCallbackProc;		// erforderlich(!) um das aktuelle Verzeichnis zu setzen
    br.ulFlags=BIF_RETURNONLYFSDIRS/*|BIF_NEWDIALOGSTYLE*/;	// Erlaubt "Neuer Ordner": Hier Quatsch! Ohne NEWDIALOG wird das aktuelle Verzeichnis gleich fokussiert!
    br.hwndOwner=Wnd;
    //br.lpszTitle=
    br.lParam=(LPARAM)p;
    LPITEMIDLIST pidl = SHBrowseForFolder(&br);
    if (pidl) {
     SHGetPathFromIDList(pidl,p);
     SetWindowText(hEdit,p);
     CoTaskMemFree(pidl);
    }
   }break;
   case 23: {	// Dateiauswahl
    HWND hEdit=GetPrevSibling((HWND)lParam);
    TCHAR p[MAX_PATH];
    GetWindowText(hEdit,p,elemof(p));
    OPENFILENAME ofn;
    ZeroMemory(&ofn,sizeof ofn);
    ofn.lStructSize=sizeof ofn;
    ofn.hwndOwner=Wnd;
    ofn.Flags=OFN_PATHMUSTEXIST|OFN_HIDEREADONLY;
    ofn.lpstrFile=p;
    ofn.nMaxFile=elemof(p);
    if (GetSaveFileName(&ofn)) {
     SetWindowText(hEdit,p);
    }
   }break;
   case MAKELONG(0xF5,1): SetForegroundWindow(hMainWnd); break;
  }break;
  case WM_DESTROY: {
   HFONT f2=(HFONT)SendDlgItemMessage(Wnd,11,WM_GETFONT,0,0);
   DeleteFont(f2);
   hSettingsDlg=0;
  }break;
 }
 return FALSE;
}

bool write_utf8(HANDLE f,const char*fmt,const TCHAR*arg) {
#ifdef UNICODE
 int l=WideCharToMultiByte(CP_UTF8,0,arg,-1,0,0,0,0);
 char*p=new char[l];
 WideCharToMultiByte(CP_UTF8,0,arg,-1,p,l,0,0);
 char s[MAX_PATH];
 l=_snprintf(s,sizeof s,fmt,p);
 DWORD bw;
 bool ret=!!WriteFile(f,s,l,&bw,0);
 delete[]p;
 return ret;
#else	// ANSI, kein UTF-8
 char s[MAX_PATH];
 int l=_snprintf(s,sizeof s,fmt,arg);
 DWORD bw;
 return !!WriteFile(f,s,l,&bw,0);
#endif
}

bool NotizAppend(HWND Wnd) {
 HANDLE f=CreateFile(note,GENERIC_WRITE,FILE_SHARE_READ,0,OPEN_ALWAYS,0,0);
 if (f==INVALID_HANDLE_VALUE) return false;
 DWORD fp=SetFilePointer(f,0,0,FILE_END);
 bool ret=true;
 if (newfile) {
  if (!write_utf8(f,"\n[%s]\n"+(fp?0:1),currentfile)) ret=false;
// Leerzeile vor neuer Section generieren, aber nicht am Dateianfang
  newfile=false;
 }
 TCHAR s[512];
 GetDlgItemText(Wnd,11,s,elemof(s));
 if (!write_utf8(f,"%s=",s)) ret=false;
 GetDlgItemText(Wnd,12,s,elemof(s));
 if (!write_utf8(f,"%s\n",s)) ret=false;
 if (!CloseHandle(f)) ret=false;
 return ret;
}

static void Zeitmarke(HWND Wnd,UINT id,bool zehntel) {
 FILETIME t;
 GetSystemTimeAsFileTime(&t);
 *(__int64*)&t-=*(__int64*)&starttime;
 SYSTEMTIME st;
 FileTimeToSystemTime(&t,&st);
 TCHAR s[32];
 int j=GetTimeFormat(LOCALE_USER_DEFAULT,TIME_FORCE24HOURFORMAT,&st,0,s,elemof(s))-1;
 if (zehntel) _sntprintf(s+j,elemof(s)-j,TEXT("%c%u"),sDecimal[0],st.wMilliseconds/100);	// Zehntelsekunden anhängen
 SetDlgItemText(Wnd,id,s);
}

static INT_PTR CALLBACK NotizDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
 switch (Msg) {
  case WM_INITDIALOG: {
   hNotizDlg=Wnd;
   restorePos(Wnd,config.posNotice);
   if (!lParam) SetForegroundWindow(Wnd);	// gestartet mit Hotkey
   SetDlgItemText(Wnd,10,currentfile);
   Zeitmarke(Wnd,11,false);
  }return TRUE;

  case WM_MOVE: savePos(Wnd,config.posNotice); break;

  case WM_ACTIVATE: if (wParam) hDialog=Wnd; break;

  case WM_COMMAND: switch (wParam) {
   case 1: if (!NotizAppend(Wnd)) {MessageBeep(0); break;}
   case 2: DestroyWindow(Wnd); break;
   case MAKELONG(0xF5,1): SetForegroundWindow(hMainWnd); break;
  }break;
  case WM_DESTROY: {
   hNotizDlg=0;
  }break;
 }
 return FALSE;
}

static void overlayVuXor(HDC hdc,int x, int y, int w, int h,bool bm2) {
 HDC dc=CreateCompatibleDC(hdc);
 HBITMAP obm,&hbmXor=hbmVuXor[bm2];
 if (!hbmXor) {
  hbmXor=CreateBitmap(w,h,1,1,0);	// Monochrome Bitmap um Skale mittels XorPut zu zeichnen
  obm=SelectBitmap(dc,hbmXor);	// Zeichenfläche einsetzen
  PatBlt(dc,0,0,w,h,BLACKNESS);	// Bitmap theoretisch undefiniert, praktisch (Win10) schwarz. Sicherheitshalber schwarz (Bits=0) setzen
  HFONT ofont=SelectFont(dc,hfntAlias);
  TEXTMETRIC tm;
  GetTextMetrics(dc,&tm);
  int ty=(h-tm.tmHeight)>>1;
  SetTextColor(dc,RGB(255,255,255));
  SetBkColor(dc,0);
  HPEN open=SelectPen(dc,GetStockPen(WHITE_PEN));
  const int scalestart=config.z.dbumfang(),// Anzeigeumfang -40 db .. 0
	    majortick=10,	// langer Strich alle 10 dB
	    minortick=2,	// kurzer Strich alle 2 dB
	    gap=2;		// Pixel Platz für Text zum Rand
  for (int i=0; i<=scalestart; i+=minortick) {
   int xx=MulDiv(i,w-1,scalestart);
   int yy=h-ty; if (i%majortick) yy+=ty>>1;
   MoveToEx(dc,xx,yy,0);
   LineTo(dc,xx,h);	// Endpixel wird nicht gezeichnet
   if (!(i%majortick)) {
    TCHAR s[4];
    int l=_sntprintf(s,elemof(s),T("%d"),i-scalestart);
    SetTextAlign(dc,i?i==scalestart?TA_RIGHT:TA_CENTER:TA_LEFT);
    TextOut(dc,i?i==scalestart?w-gap:xx:gap,ty,s,l);
   }
  }
  SetTextAlign(dc,TA_CENTER);
  const int spacex=scalestart*2/majortick;	// 8: Bei 1/8, 3/8, 5/8 und 7/8 ist Platz für die Einheit
  TextOut(dc,MulDiv(w,spacex-1,spacex),0,TEXT("dB"),2);
  SelectFont(dc,ofont);
  SelectPen(dc,open);
 }else obm=SelectBitmap(dc,hbmXor);	// Bereits erstellte Quellbitmap einsetzen
 BitBlt(hdc,x,y,w,h,dc,0,0,SRCINVERT);
 SelectBitmap(dc,obm);
 DeleteDC(dc);
}	// Dasselbe mit Pfaden zu erreichen führte zu hässlichen ausgefransten Buchstaben und Ziffern!

INT_PTR CALLBACK MainDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
 static HBRUSH hbrBlue,hbrCyan,hbrRed;
 static HMENU mym;
 static HFONT hfntBig;
 switch (Msg) {
  case WM_INITDIALOG: {
   hMainWnd=Wnd;
//   hTooltip=CreateWindowEx(WS_EX_TOPMOST,TOOLTIPS_CLASS,0,WS_POPUP|TTS_NOPREFIX|WS_VISIBLE,0,0,0,0,Wnd,0,0,0);
   HMENU sysm=GetSystemMenu(Wnd,false);
   mym=LoadMenu(hInstance,MAKEINTRESOURCE(100));
   TCHAR s[64];
   GetMenuString(mym,119,s,elemof(s),MF_BYCOMMAND);
   DestroyMenu(mym);
   AppendMenu(sysm,MF_STRING,0x40,s);
   DeleteMenu(sysm,SC_MAXIMIZE,MF_BYCOMMAND);
   DeleteMenu(sysm,SC_SIZE,MF_BYCOMMAND);
   DeleteMenu(sysm,SC_RESTORE,MF_BYCOMMAND);
   nid.cbSize=sizeof nid;
   nid.hWnd=Wnd;
   nid.uID=100;
   nid.uCallbackMessage=WM_USER+42;
   nid.uFlags=NIF_ICON|NIF_TIP|NIF_MESSAGE;
   nid.hIcon=LoadIcon(hInstance,MAKEINTRESOURCE(100));
   SetClassLongPtr(Wnd,GCLP_HICON,(LONG_PTR)nid.hIcon);
   SetClassLongPtr(Wnd,GCLP_HICONSM,(LONG_PTR)nid.hIcon);
//   GetWindowText(Wnd,nid.szTip,elemof(nid.szTip));
   Shell_NotifyIcon(NIM_ADD,&nid);	// Tip und MBoxTitel kommt später in onNinChange()
   hbrBlue=CreateSolidBrush(RGB(0,0,255));	// Effektivwert
   hbrCyan=CreateSolidBrush(RGB(0,96,255));	// Spitzenwert
   hbrRed=CreateSolidBrush(RGB(255,0,0));	// Übersteuerung
   HFONT fnt=GetWindowFont(Wnd);
//   SetWindowFont(hTooltip,fnt,false);
   LOGFONT lf;
   GetObject(fnt,sizeof lf,&lf);
   long save=lf.lfHeight;
   lf.lfHeight=MulDiv(lf.lfHeight,12,8);
   hfntBig=CreateFontIndirect(&lf);
   SendDlgItemMessage(Wnd,118,WM_SETFONT,(WPARAM)hfntBig,true);
   lf.lfHeight=save+3;
   lf.lfQuality=NONANTIALIASED_QUALITY;	// erforderlich für Pfad und Pfadfüllen, aber wirkungslos
   hfntAlias=CreateFontIndirect(&lf);
   config.z.sel=-1;		// AUDIO_MAPPER
   config.z.nch_bits=0x11;	// Stereo, 16 Bit
   config.z.flags=0x42;		// WASAPI, MP3
   config.z.rate=44;		// CD-Qualität
   config.z.mp3min=16;		// Byte(!) pro Sekunde
   config.z.mp3max=16;		// Byte(!) pro Sekunde
   for(SRCCONFIG*q=config.q; q!=config.q+MAXIN; q++) {
    q->sel=char(q-config.q);	// Laufender Index als Vorgabe
    q->nch_bits=0x11;		// Stereo, 16 Bit
    q->gain=-36;		// Lautstärke halbieren: -6 dB
    q->panorama=q-config.q&1?100:-100;	// ungerade: rechts zumischen, sonst links zumischen
   }
   PostMessage(Wnd,WM_USER+20,0,0);	// ContinueInit (TODO: Wieder weg!)
  }return TRUE;
  case WM_USER+20: {
   GetProfileString(T("Intl"),T("sDecimal"),T("."),sDecimal,elemof(sDecimal));
   WINDOWPLACEMENT wp;
   wp.length=sizeof wp;
   GetWindowPlacement(Wnd,&wp);
   mainWndWidth=wp.rcNormalPosition.right-wp.rcNormalPosition.left;	// Fensterbreite für 1..2 Quellen
   if (config.load()) {
    OffsetRect(&wp.rcNormalPosition,config.posMain.x-wp.rcNormalPosition.left,config.posMain.y-wp.rcNormalPosition.top);
    SetWindowPlacement(Wnd,&wp);	// SetWindowPlacement sollte gegenüber SetWindowPos die Monitorausdehnung beachten und das Fenster keinesfalls unsichtbar außerhalb platzieren
//    SetWindowPos(Wnd,0,config.posx,config.posy,0,0,SWP_NOSIZE|SWP_NOZORDER);
   }
// Quellenbezogene Fenster klonen für NCH Quellen
   onNinChange();
   SAVE::newall();
   if (!(saveobj[1]->okay() || saveobj[2]->okay())) PostMessage(Wnd,WM_SYSCOMMAND,0x40,0);	// Anwender muss/sollte Pfad festlegen!
   registerhotkey(Wnd,1,config.hotkey[0]);
   registerhotkey(Wnd,120,config.hotkey[1]);
   SetDlgItemText(Wnd,104,file);
   imagelist=ImageList_LoadImage(hInstance,MAKEINTRESOURCE(10),21,0,CLR_DEFAULT,IMAGE_BITMAP,0);	// Lautsprecher, Mikrofon
   imagelist11=ImageList_LoadImage(hInstance,MAKEINTRESOURCE(11),16,0,CLR_DEFAULT,IMAGE_BITMAP,0);	// Raumklangsymbolik
   FillWaveinCombos();
   FillCombo2(Wnd,101,config.z.rate);
   FillCombo(Wnd,102,102,config.z.getbytes()-1);
   FillComboEx(Wnd,103,config.z.getspat());
   FillCombo2(Wnd,124,106,config.z.mp3min);
   FillWaveoutCombo(GetDlgItem(Wnd,127));
   mixer.init(GetDlgItem(Wnd,99));
   mixer.initwf(config.z.getrate(),config.z.getchan(),config.z.getbits());
   enableoutputsel();
   setoutputsel();
   if (config.z.echoing() && mixer.connect()) {
    CheckDlgButton(Wnd,126,MF_CHECKED);	// "Parallele Audioausgabe"
   }
   onNinChange(Wnd);	// 2. Teil des Klonens: Mit Inhalten füllen
   for (int i=0; i<config.NIN; i++) {
    SRCCONFIG&q=config.q[i];
    rec[i].init(GetDlgItem(Wnd,MAKEWORD(20,i)),config.z.getrate(),q.getchan(),q.getbits());
    rec[i].connect();
   }
   if (config.z.recording() && save_start()) CheckDlgButton(Wnd,1,true);
   else config.z.recording(false);
   recordchange();
   if (config.z.maininvis()) ShowWindow(Wnd,SW_MINIMIZE);
   SetTimer(Wnd,0,250,0);
  }return TRUE;

  case WM_DEVICECHANGE: {
   SetTimer(Wnd,1,1500,0);	// WaveIn- und WaveOut-Geräteliste nach 1,5 s aktualisieren lassen
  }break;

  case WM_MOVE: savePos(Wnd,config.posMain); break;

  case WM_SIZE: {
   if (wParam==SIZE_MINIMIZED) {
    ShowWindow(Wnd,SW_HIDE);
    config.z.maininvis(true);
    return TRUE;
   }
  }break;

  case WM_ACTIVATE: if (wParam) hDialog=Wnd; break;

  case WM_DRAWITEM: {	// VU-Meter
   const DRAWITEMSTRUCT&dis=*(const DRAWITEMSTRUCT*)lParam;
   const bool sumsignal=wParam==99;
   const VU&vu=sumsignal?VU(mixer):rec[HIBYTE(wParam)];	// Anzeigedaten
   const HBRUSH hbrWhite=GetStockBrush(WHITE_BRUSH);
   const int k=vu.wf.Format.nChannels;
   if (unsigned(k-1)>=8) vu.error=111;
   if (vu.error) {
    TCHAR s[64];
    int l=LoadString(hInstance,vu.error,s,elemof(s));
    bool warning=vu.error==111;
    FillRect(dis.hDC,&dis.rcItem,warning?hbrWhite:hbrRed);
//    SetTextAlign(dis.hDC,TA_CENTER|TA_BASELINE);
    if (!warning) SetTextColor(dis.hDC,RGB(255,255,255));
    SetBkMode(dis.hDC,TRANSPARENT);
    DrawText(dis.hDC,s,l,const_cast<RECT*>(&dis.rcItem),
      DT_SINGLELINE|DT_CENTER|DT_VCENTER|DT_NOPREFIX);
   }else{
    int w=dis.rcItem.right-dis.rcItem.left,	// Gesamtbreite
        h=dis.rcItem.bottom-dis.rcItem.top,	// Gesamthöhe
        j=0;
    RECT rc; rc.bottom=dis.rcItem.top;
    do{	// Für jeden der bis zu 8 Kanäle
     rc.top=rc.bottom;				// direkt untereinander
     rc.bottom=dis.rcItem.top+MulDiv(j+1,h,k);	// Balkenhöhe
     rc.left=dis.rcItem.left;
     rc.right=dis.rcItem.left+MulDiv(vu.rms[j],w,100);	// Balkenlänge
     FillRect(dis.hDC,&rc,vu.ovl[j]?hbrRed:hbrBlue);	// Effektivwert
     rc.left=rc.right;
     rc.right=dis.rcItem.left+MulDiv(vu.pik[j],w,100);	// Balken rechts
     FillRect(dis.hDC,&rc,vu.ovl[j]?hbrRed:hbrCyan);	// Spitzenwert (immer > Effekivwert)
     rc.left=rc.right;
     rc.right=dis.rcItem.right;	// Weißraum rechts
     FillRect(dis.hDC,&rc,hbrWhite);
    }while(++j<k);
    overlayVuXor(dis.hDC,dis.rcItem.left,dis.rcItem.top,w,h,sumsignal);
   }
  }break;

  case WM_SYSCOMMAND: switch (wParam&~15) {
   case 0x40: SendMessage(Wnd,WM_COMMAND,119,0); break;
  }break;

  case WM_COMMAND: {
   int idx=HIBYTE(wParam);
   SRCCONFIG&q=config.q[idx];
   switch (wParam&~0xFF00) {
    case 1:{
     config.z.recording(2);	// toggle
     if (!lParam) CheckDlgButton(Wnd,1,config.z.recording());
     if (config.z.recording()) {
      config.save();		// bevor's abstürzt
      if (!save_start()) {
       MBox(Wnd,98,MB_OK|MB_ICONEXCLAMATION,currentfile);
       config.z.recording(false);
       CheckDlgButton(Wnd,1,BST_UNCHECKED);
      }
     }else save_stop();
     recordchange();
    }break;
    case 2: if (config.z.recording()  && MBox(Wnd,103,MB_YESNO|MB_DEFBUTTON2)!=IDYES) break;
    case 3: {
     KillTimer(Wnd,0);
     if (config.z.recording()) save_stop();
     for (REC*rp=rec;rp!=rec+config.NIN;rp++) rp->disconnect();
     if (config.z.echoing()) mixer.disconnect();
     mixer.realloc(0);
     config.save();
     DeleteBrush(hbrRed);
     DeleteBrush(hbrBlue);
     DeleteBrush(hbrCyan);
     DeleteFont(hfntBig);
     DeleteFont(hfntAlias);
     DeleteBitmap(hbmVuXor[0]);
     DeleteBitmap(hbmVuXor[1]);
//     DestroyWindow(hTooltip);
     Shell_NotifyIcon(NIM_DELETE,&nid);
     registerhotkey(Wnd,1);
     registerhotkey(Wnd,120);
     ImageList_Destroy(imagelist11);
     ImageList_Destroy(imagelist);
     SAVE::deleteall();
     DestroyWindow(Wnd);
    }break;
    case 4: if (config.z.recording()) {	// Lückenloser Dateiwechsel (nur wenn Aufnahme läuft)
     save_stop();
     if (!save_start()) {
      MBox(Wnd,98,MB_OK|MB_ICONEXCLAMATION,currentfile);
      config.z.recording(false);
      CheckDlgButton(Wnd,1,BST_UNCHECKED);
     }
    }break;
    case MAKELONG(10,CBN_SELCHANGE): {
     COMBOBOXEXITEM cbei;
     cbei.mask=CBEIF_LPARAM;
     cbei.iItem=ComboBox_GetCurSel((HWND)lParam);
     SendMessage((HWND)lParam,CBEM_GETITEM,0,(LPARAM)&cbei);
     q.sel=(char)cbei.lParam;
     rec[idx].disconnect();
     rec[idx].connect();
    }break;
    case MAKELONG(30,CBN_SELCHANGE): {	// Raumklang
     BYTE spa=(BYTE)(ComboBox_GetCurSel((HWND)lParam));
     q.setspat(spa);
     EnableDlgItem(Wnd,MAKEWORD(60,idx),spa==0 && config.z.getchan()>=2);	// Panoramaregler ein wenn Quelle mono UND Ziel stereo
     rec[idx].disconnect();
     rec[idx].init(0,0,q.getchan());
     rec[idx].connect();
    }break;
    case MAKELONG(40,BN_CLICKED): {	// AGC
     q.setAGC(2);
     EnableDlgItem(Wnd,MAKEWORD(50,idx),!q.getAGC());	// Lautstärkeregler aus wenn AGC aktiv
    }break;
    case MAKELONG(70,CBN_EDITCHANGE):
    case MAKELONG(101,CBN_EDITCHANGE):
    case MAKELONG(124,CBN_EDITCHANGE): {
     SetTimer(Wnd,LOWORD(wParam),500,0);	// Edit-Element verzögert auslesen (für mehrstellige Zahlen)
    }break;
    case MAKELONG(70,CBN_SELCHANGE):
    case MAKELONG(101,CBN_SELCHANGE):
    case MAKELONG(124,CBN_SELCHANGE): {
     SetTimer(Wnd,LOWORD(wParam),100,0);	// Edit-Element updaten lassen, dann auslesen
    }break;
    case MAKELONG(102,CBN_SELCHANGE): {		// Bitbreite Senke: Nichts weiter tun, wirkt sich erst auf nächste Aufnahme aus
     config.z.setbytes(ComboBox_GetCurSel((HWND)lParam)+1);
     mixer.initwf(0,0,config.z.getbits());
     mixer.realloc();
    }break;
    case MAKELONG(103,CBN_SELCHANGE): {		// Raumklang (ComboBoxEx mit Symbolik)
     config.z.setspat(ComboBox_GetCurSel((HWND)lParam));
     mixer.initwf(0,config.z.getchan(),0);
     mixer.realloc();
     for (int idx=0; idx<config.NIN; idx++) {
      EnableDlgItem(Wnd,MAKEWORD(60,idx),config.q[idx].getchan()==1 && config.z.getchan()>=2);	// Panoramaregler ein/aus
     }
    }break;
    case MAKELONG(104,EN_UPDATE): {
     GetWindowText((HWND)lParam,file,elemof(file));
     SetTimer(Wnd,LOWORD(wParam),500,0);	// verzögert speichern
    }break;
    case MAKELONG(105,BN_CLICKED): {
     OPENFILENAME ofn;
     TCHAR filter[128];
     TCHAR f[MAX_PATH];
     sprintDateTime(f,elemof(f),file);
     memset(&ofn,0,sizeof ofn);
     ofn.lStructSize=sizeof ofn;
     ofn.lpstrFile=f;
     ofn.nMaxFile=elemof(file);
     ofn.Flags=OFN_HIDEREADONLY|OFN_PATHMUSTEXIST;
     ofn.lpstrFilter=filter;
     ofn.nFilterIndex=config.z.getfileformat()+1;
     filter[LoadString(hInstance,105,filter,elemof(filter)-1)+1]=0;
     if (GetSaveFileName(&ofn)) {
      lstrcpyn(file,f,elemof(file));
      SetDlgItemText(Wnd,104,file);
      config.save();
     }
    }break;
    case 119: if (hSettingsDlg) SetForegroundWindow(hSettingsDlg);
    else hSettingsDlg=CreateDialog(hInstance,MAKEINTRESOURCE(119),Wnd,SettingsDlgProc); break;
    case 120: if (hNotizDlg) SetForegroundWindow(hNotizDlg);
    else if (currentfile[0]) {
     hNotizDlg=CreateDialogParam(hInstance,MAKEINTRESOURCE(120),
       lParam?Wnd:0,NotizDlgProc,lParam);	// Position bzgl. Fenster oder absolut
    }break;
    case 121:
    case 122:
    case 123: setoutputsel(LOBYTE(wParam)-121); break;
    case 126: {	// Chechbox "Parallele Ausgabe"
     if (config.z.echoing()) {
      mixer.disconnect();
      config.z.echoing(false);
     }else{
      config.z.echoing(true);
      mixer.connect();
     }
    }break;
    case MAKELONG(127,CBN_SELCHANGE): {	// ComboBoxEx "Paralleles Ausgabegerät"
     COMBOBOXEXITEM cbei;
     cbei.mask=CBEIF_LPARAM;
     cbei.iItem=ComboBox_GetCurSel((HWND)lParam);
     SendMessage((HWND)lParam,CBEM_GETITEM,0,(LPARAM)&cbei);
     config.z.sel=(char)cbei.lParam;
     if (config.z.echoing()) {
      mixer.disconnect();
      mixer.connect();
     }
    }break;
    case 100: ShowWindow(Wnd,SW_SHOWNORMAL); config.z.maininvis(false); break;
    case MAKELONG(0xF5,1): if (hNotizDlg) SetForegroundWindow(hNotizDlg);
    else if (hSettingsDlg) SetForegroundWindow(hSettingsDlg);
    break;
    case MAKELONG(0xF7,1): SendMessage(Wnd,WM_COMMAND,119,0); break;	// Einstellungen per Accelerator
    case MAKELONG(0xF8,1): SendMessage(Wnd,WM_COMMAND,120,0); break;	// Notiz per Accelerator
   }
  }break;
  case WM_HSCROLL: {
   switch (LOBYTE(wParam)) {
    case SB_ENDSCROLL:
    case SB_THUMBTRACK: {
     int pos=(int)SendMessage((HWND)lParam,TBM_GETPOS,0,0);
     int id=GetDlgCtrlID((HWND)lParam);
     SRCCONFIG&q=config.q[HIBYTE(id)];
     switch (LOBYTE(id)) {
      case 50: q.gain=(char)pos; break;
      case 60: q.panorama=(char)pos; break;
     }
    }break;
   }
  }break;

  case WM_NOTIFY: {
   NMHDR&nmhdr=*(NMHDR*)lParam;
   if (nmhdr.code==TTN_NEEDTEXT) {
    TOOLTIPTEXT&ttt=*(TOOLTIPTEXT*)lParam;
    if (ttt.uFlags&TTF_IDISHWND) {
     DWORD id=GetDlgCtrlID((HWND)ttt.hdr.idFrom);
     if (LOBYTE(id)==50) {	// Verstärkungs-Slider (1 pro Quelle)
      int pos=(int)SendMessage((HWND)ttt.hdr.idFrom,TBM_GETPOS,0,0);
      _sntprintf(ttt.szText,elemof(ttt.szText),T("%d dB"),MulDiv(pos,20,120));
     }
    }
   }
  }break;

  case WM_TIMER: if (wParam) {		// Combobox-Edit-Eingaben und Combobox-Auswahl verzögert behandeln
   KillTimer(Wnd,wParam);
   int idx=HIBYTE(wParam);
   SRCCONFIG&q=config.q[idx];
   REC&r=rec[idx];
   BYTE v=(BYTE)GetDlgItemInt(Wnd,int(wParam),0,false);	// entweder Bits (wParam==102, 70) oder Rate (wParam==101)
   BYTE cb;	// Vorhergehender Wert
   switch (LOBYTE(wParam)) {
    case 1: FillWaveinCombos(); FillWaveoutCombo(GetDlgItem(Wnd,127)); return FALSE;
    case 101: cb=config.z.rate; break;
    case 124: cb=config.z.mp3min; break;
    default: cb=q.getbits();
   }
   if (cb==v && !lParam) break;		// Nichts tun wenn sich nichts ändert
   switch (LOBYTE(wParam)) {
    case 101: {			// Rate: Alle Quellen schließen und öffnen
     config.z.rate=v;
     CloseOpenAllWaveIn(config.z.getrate());
    }break;
    case 124: {
     config.z.mp3min=v;
    }break;
    case 104: break;	// Dateiname … nur speichern!
    default: {			// Bitbreite Quelle: Nur betreffende Quelle schließen und öffnen
     q.setbits(v);
     r.disconnect();
     r.init(0,0,0,v);
     r.connect();
    }
   }
   config.save();
  }else{
   for (int i=0; i<config.NIN; i++) rec[i].alive();	// Überlebensstatus anzeigen
  }break;
  case WM_HOTKEY: SendMessage(Wnd,WM_COMMAND,wParam,0); break;	// Mit 3 Modifiern geht's
  case WM_USER+42: switch (lParam) {
   case WM_LBUTTONDOWN: //wasapi::record();	// DEBUG!!
   case WM_RBUTTONDOWN: {
    HMENU m=LoadMenu(hInstance,MAKEINTRESOURCE(100));
    EnableMenuItem(m,120,config.z.recording()?MF_ENABLED:MF_GRAYED);
    POINT pt;
    GetCursorPos(&pt);
    TrackPopupMenu(GetSubMenu(m,0),TPM_RIGHTBUTTON,pt.x,pt.y,0,Wnd,0);
    DestroyMenu(m);
   }break;
   case WM_LBUTTONDBLCLK: SendMessage(Wnd,WM_COMMAND,100,0); break;
  }break;
  case WM_USER+23: {
   Zeitmarke(Wnd,118,true);
  }break;
  case WM_QUERYENDSESSION: if (config.z.recording()) switch (MBox(Wnd,103,MB_YESNO|MB_DEFBUTTON2)) {
   case IDNO: {
    SetWindowLongPtr(Wnd,DWLP_MSGRESULT,0);	// deny termination
   }return TRUE;
  }break;
  case WM_ENDSESSION: {
   if (config.z.recording()) save_stop();
   config.save();
  }break;
  case WM_DESTROY: PostQuitMessage(0); break;
 }
 return FALSE;
}

void WinMainCRTStartup() {
 hInstance=GetModuleHandle(0);
 INITCOMMONCONTROLSEX ice={sizeof ice,ICC_USEREX_CLASSES|ICC_WIN95_CLASSES};
 InitCommonControlsEx(&ice);
 UINT exitcode=IDCANCEL;
 if (!CoInitializeEx(0,COINIT_MULTITHREADED)) {
  CreateDialog(hInstance,MAKEINTRESOURCE(100),0,MainDlgProc);
  HACCEL hAccel=LoadAccelerators(hInstance,MAKEINTRESOURCE(100));
  MSG Msg;
  while (GetMessage(&Msg,0,0,0)) {
   if (hDialog && TranslateAccelerator(hDialog,hAccel,&Msg)) continue;
   if (hDialog && IsDialogMessage(hDialog,&Msg)) continue;
   TranslateMessage(&Msg);
   DispatchMessage(&Msg);
  }
  exitcode=(UINT)Msg.wParam;
  CoUninitialize();
 }
 ExitProcess(exitcode);
}

// VS2019-64-Bit-Compiler bei /NODEFAULTLIB glücklich machen
#if _MSC_VER >= 1900
void _cdecl operator delete(void*a,size_t) {free(a);}
#endif

#ifdef _M_IX86
extern "C"{
#if _MSC_VER<1500
// mangels Laufzeitbibliothek "longlong"-Routinen nachreichen,
// diese erwartet in EDX:EAX das "longlong" und in CL die Schiebeweite
// "_cdecl" notwendig, schaltet Namensgarnierung zurück auf '_'-Präfix
// Int64ShrlMod32 usw. sind weniger effektiv als diese Routinen!
LONGLONG _declspec(naked) _cdecl _allshr(LONGLONG ll, BYTE shift) {
 _asm{
//	test	cl,32
//	jnz	l1
	shrd	eax,edx,cl
	sar	edx,cl
	ret
//l1:	mov	eax,edx
//	xor	edx,edx
//	sar	eax,cl
//	ret
 }
}
#else
_declspec(dllimport) int _cdecl localtime();
FARPROC _imp___localtime64=(FARPROC)localtime;
_declspec(dllimport) int _cdecl time();
FARPROC _imp___time64=(FARPROC)time;
#endif
}
#endif
Detected encoding: ANSI (CP1252)4
Wrong umlauts? - Assume file is ANSI (CP1252) encoded