Introduction
This is an MP3 + CDG file player application that parses CDG files and renders them in real time while an MP3 file is played. As a bonus, I included an MP3 + CDG to AVI conversion application which will not be discussed in this article.
Background
After looking on the web for a while, I noticed that there was no .NET implementation for CDG file parsing available. One thing led to another, and I started porting over some C++ code that I found on Google Code. Once playback was established, I wanted pitch shifting. After that, I wanted rendering to AVI files. After that, I wanted transparency and background video for the AVI rendering...
Using the Code
This is the high level workflow for playing MP3 and CDG files together:
- Unzip the MP3 and CDG files into a temporary directory.
- Create a stream and open the CDG file.
- Determine the duration of the CDG file so that boundaries can be defined.
- Start playback of the MP3 file.
- Render the frame of the CDG file that is at the elapsed time position of the MP3 that is playing.
The main playback rendering block looks like this:
Private Sub Play()
Try
If mMP3Stream <> 0 AndAlso Bass.BASS_ChannelIsActive(mMP3Stream) = _
BASSActive.BASS_ACTIVE_PLAYING Then
StopPlayback()
End If
PreProcessFiles()
If mCDGFileName = "" Or mMP3FileName = "" Then
MsgBox("Cannot find a CDG and MP3 file to play together.")
StopPlayback()
Exit Sub
End If
mPaused = False
mStop = False
mFrameCount = 0
mCDGFile = New CDGFile(mCDGFileName)
Dim cdgLength As Long = mCDGFile.getTotalDuration
PlayMP3Bass(mMP3FileName)
Dim startTime As DateTime = Now
Dim endTime = startTime.AddMilliseconds(mCDGFile.getTotalDuration)
Dim millisecondsRemaining As Long = cdgLength
While millisecondsRemaining > 0
If mStop Then
Exit While
End If
millisecondsRemaining = endTime.Subtract(Now).TotalMilliseconds
Dim pos As Long = cdgLength - millisecondsRemaining
While mPaused
endTime = Now.AddMilliseconds(millisecondsRemaining)
Application.DoEvents()
End While
mCDGFile.renderAtPosition(pos)
mFrameCount += 1
mCDGWindow.PictureBox1.Image = mCDGFile.RGBImage
mCDGWindow.PictureBox1.BackColor = CType(mCDGFile.RGBImage, Bitmap).GetPixel(1, 1)
mCDGWindow.PictureBox1.Refresh()
Dim myFrameRate As Single = Math.Round(mFrameCount / (pos / 1000), 1)
Application.DoEvents()
End While
StopPlayback()
Catch ex As Exception
End Try
End Sub
Another challenge that presented itself was taking a multidimensional array of integers that was created from the CDG library and converting it into a bitmap. Here is the code that does the conversion:
Public ReadOnly Property RGBImage(Optional ByVal makeTransparent _
As Boolean = False) As System.Drawing.Image
Get
Dim temp As New MemoryStream
Try
Dim i As Integer = 0
For ri = 0 To CDG_FULL_HEIGHT - 1
For ci = 0 To CDG_FULL_WIDTH - 1
Dim ARGBInt As Integer = m_pSurface.rgbData(ri, ci)
Dim myByte(3) As Byte
myByte = BitConverter.GetBytes(ARGBInt)
temp.Write(myByte, 0, 4)
Next
Next
Catch ex As Exception
End Try
Dim myBitmap As Bitmap = StreamToBitmap(temp, CDG_FULL_WIDTH, CDG_FULL_HEIGHT)
If makeTransparent Then
myBitmap.MakeTransparent(myBitmap.GetPixel(1, 1))
End If
Return myBitmap
End Get
End Property
Public Shared Function StreamToBitmap(ByRef stream As Stream, _
ByVal width As Integer, ByVal height As Integer) As Bitmap
Dim bmp As New Bitmap(width, height, PixelFormat.Format32bppArgb)
Dim bmpData As BitmapData = bmp.LockBits(New Rectangle(0, 0, width, height), _
ImageLockMode.[WriteOnly], bmp.PixelFormat)
stream.Seek(0, SeekOrigin.Begin)
For n As Integer = 0 To stream.Length - 1
Dim myByte(0) As Byte
stream.Read(myByte, 0, 1)
Marshal.WriteByte(bmpData.Scan0, n, myByte(0))
Next
bmp.UnlockBits(bmpData)
Return bmp
End Function
The BASS Sound library was used for playback so the pitch of the MP3 could be changed in real time to adjust to the singer's key. Volume control was also needed to be able to mix volume levels from the MP3 with the singer's microphone. Here is the code that allows for the playback, volume control, and real-time pitch shifting:
Private Sub PlayMP3Bass(ByVal mp3FileName As String)
If mBassInitalized OrElse Bass.BASS_Init(-1, 44100, _
BASSInit.BASS_DEVICE_DEFAULT, Me.Handle) Then
mMP3Stream = 0
mMP3Stream = Bass.BASS_StreamCreateFile(mp3FileName, 0, 0, _
BASSFlag.BASS_STREAM_DECODE Or _
BASSFlag.BASS_SAMPLE_FLOAT Or BASSFlag.BASS_STREAM_PRESCAN)
mMP3Stream = AddOn.Fx.BassFx.BASS_FX_TempoCreate(mMP3Stream, _
BASSFlag.BASS_FX_FREESOURCE Or BASSFlag.BASS_SAMPLE_FLOAT _
Or BASSFlag.BASS_SAMPLE_LOOP)
If mMP3Stream <> 0 Then
AdjustPitch()
AdjustVolume()
ShowCDGWindow()
Bass.BASS_ChannelPlay(mMP3Stream, False)
Else
Throw New Exception(String.Format("Stream error: {0}", _
Bass.BASS_ErrorGetCode()))
End If
End If
End Sub
Private Sub StopPlaybackBass()
Bass.BASS_Stop()
Bass.BASS_StreamFree(mMP3Stream)
Bass.BASS_Free()
mMP3Stream = 0
mBassInitalized = False
End Sub
Private Sub AdjustPitch()
If mMP3Stream <> 0 Then
Bass.BASS_ChannelSetAttribute(mMP3Stream, _
BASSAttribute.BASS_ATTRIB_TEMPO_PITCH, nudKey.Value)
End If
End Sub
Private Sub AdjustVolume()
If mMP3Stream <> 0 Then
Bass.BASS_ChannelSetAttribute(mMP3Stream, _
BASSAttribute.BASS_ATTRIB_VOL, _
If(trbVolume.Value = 0, 0, (trbVolume.Value / 100)))
End If
End Sub
Points of Interest
I would like to thank:
- Nikolay Nikolov for writing the original CDG code in C++
- The authors of the BASS sound library
- Anyone else whose code snippets I may have borrowed
History