Odtwarzanie wav - odpowiednie wysyłanie danych do karty muzycznej

0

Witam!

Wziąłem się za odtwarzanie plików wav i trafiłem na przeszkodę, mianowicie wysyłanie próbek do karty muzycznej.
Mógłbym wysłać cały utwór naraz, lecz wiąże się to z dosyć długim czasem oczekiwania na wysłanie i bardzo dużym zużyciem pamięci (197MB ze względu na tablicę, którą wypełniam wartościami do wysłania).
Chciałem wysyłać to wszystko w częściach po 1000 bajtów, a właściwie 2x1000, ale problem w tym, że dźwięk wysyłany taką metodą jest mocno przyspieszony i są przeskoki:

void LoadBuffer(PVOID pvoid) 
{
	file.seekg(44, ios::beg); 
	do	
	{
		short int* outAudioBuf = new short int[1000];
		short int buffer;
		
		for(int j = 0; j < 1000; j++)
		{
			file.read((char*)&buffer, sizeof(short int));
			outAudioBuf[j] = buffer;
		}
		outBuffer.lpData = (LPSTR)(outAudioBuf);
		outBuffer.dwBufferLength = 1000;
		outBuffer.dwFlags = 0;

		int err;
		if (err = waveOutPrepareHeader(outHandle, &outBuffer, sizeof(WAVEHDR)))
			cout << "Błąd przygotowania struktury!" << endl;

		MMRESULT rezultat = waveOutWrite(outHandle, &outBuffer, sizeof(WAVEHDR));
		size -= 1000;
		
		delete [] outAudioBuf;
	}while(size > 0);
} 

Jak podejść poprawie do wysyłania tego?
Proszę o pomoc.

0
  1. Nie używaj sizeof(short int) do odczytu próbek. Daj na sztywno 2 (16bit).
  2. Nie wiem co to jest outBuffer, ale czy masz tam dobrze ustawiony sample rate?
  3. Za MSDN: "Preparing a header that has already been prepared has no effect, and the function returns zero".

Ogólnie: przyspieszone odtwarzanie może wystąpić w przypadku, gdy urządzenie spodziewa się danych z większym bitrate, a Ty karmisz go w mniejszym (np. urządzenie jest ustawione na 48kHz, a Ty mu wbijasz dane próbkowane częstotliwością 44.1kHz).
Inny przypadek to przekazywanie/odczytywanie co którejś próbki. Np. Urządzenie może oczekiwać innych danych w buforze (ramki zamiast próbek), możesz je źle odczytywać z pliku, możesz mieć błędnie ustawioną liczbę bitów na próbkę w strukturze itp.
Przyjrzyj się zawartości struktury outBuffer i danym wejściowym. Pewnie tam siedzi babol.

0

Aha, jeszcze jedno... Po kiego w każdej iteracji pętli tworzysz i niszczysz outAudioBuf? Podobnie z buffer.

outBuffer.dwBufferLength = 1000;

Jesteś pewien, że za każdym razem odczytasz pełne 1000 bajtów? A co jeśli rozmiar pliku nie będzie wielokrotnością 1000?

0

Witam ponownie!
Dziękuję za odpowiedź.

Trochę poprawiłem kod i jest znacznie lepiej :)
Utwór jest już w tempie, ładuję po trochu. Dodałem Callback, w którym pobieram komunikat o wysłaniu wszystkiego do karty muzycznej i wrzucam kolejne.
Pojawił się pewien problem. Co jakiś czas przy kolejnym ładowaniu słyszę jakby stuknięcie, szarpnięcie, że jednak ładuje te dane i się nie wyrabia. Tak na prawdę nie ma zbyt dużego przeskoku, ale jest to stuknięcie, szarpnięcie, co jest strasznie denerwujące.

 
HWAVEOUT outHandle;
WAVEHDR outBuffer;
fstream file;
short int outAudioBuf[200000];

void LoadBuffer(PVOID pvoid) 
{		
	int err;
	file.read((char*)&outAudioBuf, 200000);
	outBuffer.lpData = (LPSTR)(outAudioBuf);
	outBuffer.dwBufferLength = file.gcount();
	outBuffer.dwFlags = 0;

	if (file.gcount() == 0)
		return;

	if (err = waveOutPrepareHeader(outHandle, &outBuffer, sizeof(WAVEHDR)))
		cout << "Błąd przygotowania struktury!" << endl;	
		
	MMRESULT rezultat = waveOutWrite(outHandle, &outBuffer, sizeof(WAVEHDR));
}

void CALLBACK waveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
{
	if (uMsg == WOM_DONE)
		_beginthread(LoadBuffer, 0, 0);
}

Jak można uniknąć tego skoku? Próbowałem z dwoma wątkami ładującymi, ale nie wyszło.
Proszę o pomoc.

0

Żeby pozbyć się tego "stukania", musisz działać na co najmniej dwóch buforach. W momencie, gdy jeden bufor jest odtwarzany, drugi powinien być przygotowywany do odtworzenia, tzn. wypełniony nową porcją danych i zakolejkowany w porcie/urządzeniu audio. Kiedy port zwróci pierwszy bufor, natychmiast zacznie odtwarzać ten drugi. W tym czasie aplikacja ma trochę czasu na przygotowanie bufora pierwszego i ponowne wysłanie go do portu.

0

Witam!
Zrobiłem to wszystko na dwóch buforach. Trochę mniej się tnie, ale jednak.

 
HWAVEOUT outHandle;
WAVEHDR outBuffer;
fstream file;
short int outAudioBuf[400000];
short int outAudioBuf2[400000];
short int outBuf[400000];
int first = 0, second = 0;

void SetSecond(PVOID pvoid)
{
	file.read((char*)&outAudioBuf2, 300000);
}

void SetFirst(PVOID pvoid)
{
	file.read((char*)&outAudioBuf, 300000);
}

inline void Copy(void)
{
	if (!second)
	{
		outBuffer.lpData = (LPSTR)(outAudioBuf2);
		second++;
	}
	else
	{
		outBuffer.lpData = (LPSTR)(outAudioBuf);
		second = 0;
	}
}

void LoadBuffer(PVOID pvoid) 
{		
	int err;	
	if (first == 0)
	{
		file.read((char*)outBuf, 300000);	
		outBuffer.lpData = (LPSTR)(outBuf);
	}
	else
		Copy();

	if (second == 0)
		_beginthread(SetSecond, 0, 0);
	else
		_beginthread(SetFirst, 0, 0);
		
	outBuffer.dwBufferLength = (int)file.gcount();		
	outBuffer.dwFlags = 0;

	if (file.gcount() == 0)
		return;	

	if (err = waveOutPrepareHeader(outHandle, &outBuffer, sizeof(WAVEHDR)))
		cout << "Błąd przygotowania struktury!" << endl;	
		
	first++;
	MMRESULT rezultat = waveOutWrite(outHandle, &outBuffer, sizeof(WAVEHDR));	
}

void CALLBACK waveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
{
	if (uMsg == WOM_DONE)
		_beginthread(LoadBuffer, 0, 0);
}

Zostały jeszcze lekkie przeskoki.
Da radę coś w tym kodzie jeszcze poprawić?

Proszę o pomoc.

0

Nie znam się na programowaniu pod Windows, więc wybaczcie głupie pytania. Czy waveOutWrite() jest synchroniczna, czy asynchroniczna? Jeśli synchroniczna, to żaden callback nie powinien być potrzebny, ani tym bardziej spawnowanie co chwila wątku tylko po to, żeby wstrzyknąć mniej niż 200kB. Pamiętaj, że 1 sekunda danych PCM (44.1kHz, 16bit, stereo) to jest 176400 bajtów, zatem przy tej wielkości bufora, co około 1.14 sekundy odpalasz wątek, a AFAIR pod Windows jest to dość kosztowne.
Jestem też sceptyczny co do tego double bufferingu. Nośnik źródła musiałby być strasznie powolny żeby program przestał się wyrabiać z pompowaniem danych. Co w tym zdaniu: "Co jakiś czas przy kolejnym ładowaniu" oznacza "kolejne ładowanie"? Zapełnianie bufora kolejną porcją próbek, czy rozpoczęcie odczytywania kolejnego/tego samego pliku?

0
Kumashiro napisał(a)

Nie znam się na programowaniu pod Windows, więc wybaczcie głupie pytania. Czy waveOutWrite() jest synchroniczna, czy asynchroniczna? Jeśli synchroniczna, to żaden callback nie powinien być potrzebny, ani tym bardziej spawnowanie co chwila wątku tylko po to, żeby wstrzyknąć mniej niż 200kB. Pamiętaj, że 1 sekunda danych PCM (44.1kHz, 16bit, stereo) to jest 176400 bajtów, zatem przy tej wielkości bufora, co około 1.14 sekundy odpalasz wątek, a AFAIR pod Windows jest to dość kosztowne.
Jestem też sceptyczny co do tego double bufferingu. Nośnik źródła musiałby być strasznie powolny żeby program przestał się wyrabiać z pompowaniem danych. Co w tym zdaniu: "Co jakiś czas przy kolejnym ładowaniu" oznacza "kolejne ładowanie"? Zapełnianie bufora kolejną porcją próbek, czy rozpoczęcie odczytywania kolejnego/tego samego pliku?

waveOutWrite() jest asynchroniczna. "Kolejne ładowanie" oznacza zapełnienie bufora kolejną porcją próbek z pliku.
Zwiększając bufor do 1764000 i tak słychać szarpnięcie.

1

Trochę mniej się tnie, ale jednak.

Tnie się, bo źle to zrobiłeś. Wprawdzie zrobiłeś dwa bufory, ale port działa tylko na jednym. No i co Ty masz z tym _beginthread?! Pisząc o dwóch buforach miałem na myśli dwa bufory w postaci dwóch obiektów WAVEHDR. Każdy ma przypisany swój kawałek pamięci, gdzie przechowuje próbki.

Czyli robisz w skrócie tak:

  • tworzysz bufory (waveOutPrepareHeader)
  • wypełniasz oba bufory próbkami.
  • kolejkujesz oba bufory (waveOutWrite)
  • jeśli port zwróci bufor (np. w WOM_DONE), wypełniasz go kolejnymi danymi i kolejkujesz ponownie. (waveOutPrepareHeader i waveOutWrite)

Do tego wystarczy jeden (dodatkowy) wątek!

p.s. ważna sprawa, microsoft nie zaleca, żeby w waveOutProc dokonywać jakichkolwiek operacji plikowych. Z wnętrza tej funkcji możesz jedynie zasygnalizować innym wątkom, że masz wolny bufor. Zatem radzę użyć mechanizmu CALLBACK_THREAD. Czyli tworzysz wątek z pętlą komunikatów, podobną do tej okienkowej (GetMessage), i w niej obsługujesz komunikat MM_WOM_DONE.

0

Jestem też sceptyczny co do tego double bufferingu. Nośnik źródła musiałby być strasznie powolny żeby program przestał się wyrabiać z pompowaniem danych.

Jeśli port zwróci bufor, to znaczy, że albo zaczął odtwarzać następny zakolejkowany, albo po prostu przestał odtwarzać cokolwiek. Wtedy teoretycznie miałbyś mniej niż 23μs (dla 44.1kHz) na wypełnienie bufora danymi i ponowne wpuszczenie go do portu. Taka dokładność pod windowsem/linuksem jest nierealna do uzyskania.

0

Witam ponownie!
Próbowałem na różne sposoby zrobić to odtwarzanie i wyszło na to, że funkcja waveOutWrite() zbyt długo się wykonuje. Wypełniłem dwa bufory, bez żadnych ifów i kombinacji zostawiłem tylko wspomnianą funkcję w komunikacie i był przeskok.

 HWAVEOUT outHandle;
WAVEHDR outBuffer, outBuffer2, outBuffer3;
fstream file;
short int outAudioBuf[1764000];
short int outAudioBuf2[1764000];
short int outAudioBuf3[1764000];
bool first = false;
HANDLE m_hAudioOut;
DWORD m_dwAudioOutId;

void PrepareNext(PVOID pvoid)
{
	if (!first)
	{
		file.read((char*)&outAudioBuf2, 1764000);
		outBuffer2.lpData = (LPSTR)(outAudioBuf2);
		outBuffer2.dwBufferLength = (int)file.gcount();		
		outBuffer2.dwFlags = 0;
	
		if (file.gcount() == 0)
			return;	

		int err;
		if (err = waveOutPrepareHeader(outHandle, &outBuffer2, sizeof(WAVEHDR)))
			cout << "Błąd przygotowania struktury!" << endl;
		
		first = true;
		cout << "1" << endl;
	}
	else
	{
		file.read((char*)&outAudioBuf, 1764000);
		outBuffer3.lpData = (LPSTR)(outAudioBuf);
		outBuffer3.dwBufferLength = (int)file.gcount();		
		outBuffer3.dwFlags = 0;
	
		if (file.gcount() == 0)
			return;	

		int err;
		if (err = waveOutPrepareHeader(outHandle, &outBuffer3, sizeof(WAVEHDR)))
			cout << "Błąd przygotowania struktury!" << endl;

		first = false;
		cout << "2" << endl;
	}
}

DWORD WINAPI AudioOutThreadProc(LPVOID lpParameter)
{
	MSG msg;
	while(GetMessage(&msg,0,0,0))
	{
		switch(msg.message )
		{
		case WOM_OPEN:
			cout << "Port otwarty!" << endl;
			break;
		case WOM_CLOSE:
			cout << "Port zamkniety!" << endl;
			break;
		case WOM_DONE:
			if (!first)
			{
				MMRESULT rezultat = waveOutWrite(outHandle, &outBuffer3, sizeof(WAVEHDR));
				_beginthread(PrepareNext, 0, 0);
			}
			else
			{
				MMRESULT rezultat = waveOutWrite(outHandle, &outBuffer2, sizeof(WAVEHDR));
				_beginthread(PrepareNext, 0, 0);
			}
			break;
		}
	}
	return msg.wParam;
}

void LoadBuffer(PVOID pvoid) 
{		
	file.read((char*)outAudioBuf, 1764000);	
	outBuffer.lpData = (LPSTR)(outAudioBuf);
	outBuffer.dwBufferLength = (int)file.gcount();		
	outBuffer.dwFlags = 0;
	
	if (file.gcount() == 0)
		return;	

	int err;
	if (err = waveOutPrepareHeader(outHandle, &outBuffer, sizeof(WAVEHDR)))
		cout << "Błąd przygotowania struktury!" << endl;

	_beginthread(PrepareNext, 0, 0);		
	MMRESULT rezultat = waveOutWrite(outHandle, &outBuffer, sizeof(WAVEHDR));
}

Na pierwszy ogień idzie wątek LoadBuffer, który jest wywoływany jeden jedyny raz, a reszta to już komunikaty i wątek zapełniający.

Nie wiem jak ogarnąć tą funkcję.
Proszę o pomoc.

0

andros, jeszcze raz zapytam, po jakiego grzyba tworzysz wątki do wypełnienia bufora? Co, niby że szybciej się wykona? Bzdura!

Tu masz przykład funkcji:

DWORD WINAPI AudioOutThreadProc(LPVOID lpParameter)
{
	MSG msg;
	WAVEHDR* hdr;
	HWAVEOUT hwo;
	
	while(GetMessage(&msg, 0, 0, 0))
	{
		switch(msg.message )
		{
		case MM_WOM_DONE:
			hdr = (WAVEHDR*)msg.lParam;
			hwo = (HWAVEOUT)msg.wParam;
			
			file.read((char*)hdr->lpData, 1764000);
			if(file.gcount())
			{
				hdr->dwBufferLength = (int)file.gcount();                
				hdr->dwFlags = 0;
				waveOutPrepareHeader(hwo, hdr, sizeof(WAVEHDR));
				waveOutWrite(hwo, hdr, sizeof(WAVEHDR));
			}

			break;

			...
		}
	}
	
	return 0;
}

Mogłem się gdzieś machnąć, pisałem z palca.

Pisząc kod założyłem, że gdzieś na początku, przy tworzeniu/preparowaniu buforów przypisujesz wszystkim polom lpData odpowiednie tablice.

0
0x666 napisał(a)

andros, jeszcze raz zapytam, po jakiego grzyba tworzysz wątki do wypełnienia bufora? Co, niby że szybciej się wykona? Bzdura!

Tu masz przykład funkcji:

....

Mogłem się gdzieś machnąć, pisałem z palca.

Pisząc kod założyłem, że gdzieś na początku, przy tworzeniu/preparowaniu buforów przypisujesz wszystkim polom lpData odpowiednie tablice.

Dziękuję za odpowiedź.
Twój kod także się tnie. Działa tak samo jak mój (mój pobiera mniej pamięci :P ).

Tworząc wątki nie myślałem o szybszym wykonaniu, tylko o wykonywaniu tego (zapełnianiu) w tle tak, żeby program nie musiał oczekiwać na te zapełnienie, aż się skończy.

0

Twój kod także się tnie.

Problem leży w LoadBuffer (przeoczyłem ją). Przeczytaj jeszcze raz ze zrozumieniem trzy pierwsze punkty, które napisałem wcześniej. Jeśli zrobisz tak jak podałem, jesteś w domu ;)

Tworząc wątki nie myślałem o szybszym wykonaniu, tylko o wykonywaniu tego (zapełnianiu) w tle tak, żeby program nie musiał oczekiwać na te zapełnienie, aż się skończy.

Jak to?! Przecież AudioOutThreadProc jest funkcją nowego wątka, więc jakim cudem pętla wewnątrz niej może blokować aplikację? Chyba czegoś tutaj nie rozumiem...

--- dodane ---

mój pobiera mniej pamięci

Czyżby? A skąd ten wniosek?

0
0x666 napisał(a)

Twój kod także się tnie.

Problem leży w LoadBuffer (przeoczyłem ją). Przeczytaj jeszcze raz ze zrozumieniem trzy pierwsze punkty, które napisałem wcześniej. Jeśli zrobisz tak jak podałem, jesteś w domu ;)

Tworząc wątki nie myślałem o szybszym wykonaniu, tylko o wykonywaniu tego (zapełnianiu) w tle tak, żeby program nie musiał oczekiwać na te zapełnienie, aż się skończy.

Jak to?! Przecież AudioOutThreadProc jest funkcją nowego wątka, więc jakim cudem pętla wewnątrz niej może blokować aplikację? Chyba czegoś tutaj nie rozumiem...

--- dodane ---

mój pobiera mniej pamięci

Czyżby? A skąd ten wniosek?

Witam!

Wniosek stąd, że w Menadżerze zadań przy Twoim kodzie wskazywał 18MB, przy moim 7MB :)

Dziękuję za pomoc.
Wszystko działa tak jak należy. Przeoczyłem możliwość kolejkowania.

0

Wniosek stąd, że w Menadżerze zadań przy Twoim kodzie wskazywał 18MB, przy moim 7MB

Tym akurat bym się nie sugerował zbytnio...

p.s. nie cytuj całych postów, bo nie ma takiej potrzeby. Cytuj tylko fragment, żeby było wiadomo, do czego się odnosisz.

1 użytkowników online, w tym zalogowanych: 0, gości: 1