Interactive Midi - Midi Parser & Player

Interactive Midi

InteractiveMidi is a module that takes the buffer of a midi file and allows you to play music with them. It comes with a Parser, Player, Visualizer, and Note Player.

Preview

Methods:

Player

Read Only

Player.IsPlayer: boolean
Player.IsPlaying: boolean
Player.TimeLength: boolean

Editable Values

Player.Loop: boolean
Player.TimeMultiplier: boolean

Functions

Player:GetSongLength() -- To get the TimeLength you do Player.TimeLength after doing this
Player:Load(MidiClass)
Player:FireEvent(EventName, ...)
Player:SetTempo(tempo: number)
Player:HeartbeatStep(MidiClass, heartBeatDeltaTime: number)
Player:SetTimePosition(timePosition: number)
Player:Stop()
Player:Pause()
Player:Play()
Parser

Read Only

IsParser: boolean
ParseType: boolean

Editable Values
Parser.Events: {}

Functions

Parser:Load(Midi)
Parser:Parse(ParseType: string)
Midi

Read Only

IsMidi: boolean
Raw: {}
Header: {}

Editable Values
None

Functions

Midi.new(...)
Midi:Load(Buffer)
Binder

Read Only

IsBinder: boolean
BindingOrder: {}

Editable Values
None

Functions

Binder:Bind(...)
Binder:Load(Buffer)
Notator

Read Only
None

Editable Values

Instruments: {}
Percussion: {}

Functions

Notator:AddInstruments(instruments: { { SoundId: string, Offset: number } })
Notator:IsPercussion(noteNumber)
Notator:CreateSound(
	instrument: string,
	settings: {
		NoteNumber: number,
		Velocity: number,
		Offset: number, -- Optional
		CheckForPercussion: boolean
	}
)
Visualizer

Read Only
None

Editable Values
None

Functions

Visualizer:Basic(
	settings: {
		NoteNumber: number,
		Length: number,
		CenterCFrame: CFrame,
		
		Size: number,
		
		Z: number,
		
		Sharp: number,
		C: number
	}
) : BlockData

Editable Values (BlockData)

NoteLetter: number
Block: Instance

Functions (BlockData)

BlockData:Stop()
BlockData:SmoothHide(tweenTime)
BlockData:SmoothVisibility(tweenTime)
BlockData:StopGrow()
BlockData:MoveUp(height: number, tweenTime: number, grows: boolean)

How to use:

Parse Midi
local InteractiveMidi = require(script.InteractiveMidi) -- Require the InteractiveMidi Module

local MidiUrl = "https://github.com/Wyvern-Dev/Wyvern/raw/main/betterviva.mid" -- URL to Midi File, this is mine to Viva la Vida
local MidiBuffer = game:GetService("HttpService"):GetAsync(MidiUrl)

local Midi, Parser = InteractiveMidi.new("Midi", "Parser")

Midi:Load(MidiBuffer) -- Load The Buffer of The Midi File
Parser:Load(Midi) -- Load the Midi (getting it ready to parse)
Parser:Parse("Summation") --  Parses the Midi, there is only "Summation" currently, so you're kinda forced to put it.

print(Parser.Instructions) -- Print the Instructions

Shortened Version

local InteractiveMidi = require(script.InteractiveMidi) -- Require the InteractiveMidi Module

local MidiUrl = "https://github.com/Wyvern-Dev/Wyvern/raw/main/betterviva.mid" -- URL to Midi File, this is mine to Viva la Vida
local MidiBuffer = game:GetService("HttpService"):GetAsync(MidiUrl)

local Midi, Parser, Binder = InteractiveMidi.new("Midi", "Parser", "Binder")
Binder:Bind(Midi, { Parser, "Parse", "Summation" })
Binder:Load(MidiBuffer)

print(Parser.Instructions) -- Print the Instructions
Midi Player
local InteractiveMidi = require(script.InteractiveMidi) -- Require the InteractiveMidi Module

local MidiUrl = "https://github.com/Wyvern-Dev/Wyvern/raw/main/betterviva.mid" -- URL to Midi File, this is mine to Viva la Vida
local MidiBuffer = game:GetService("HttpService"):GetAsync(MidiUrl)

local Midi, Parser, Player, Notator = InteractiveMidi.new("Midi", "Parser", "Player", "Notator")

Midi:Load(MidiBuffer) -- Load The Buffer of The Midi File
Parser:Load(Midi) -- Load the Midi (getting it ready to parse)
Parser:Parse("Summation") --  Parses the Midi, there is only "Summation" currently, so you're kinda forced to put it.

Player:Load(Parser) -- Load The Parser into the Player

Player.Events.NoteOn = function(Instruction) -- Listen for When a note is played
	local IsPercussion = Instruction.Channel == 9 -- 9 is for Percussion notes
	
	local Sound: Sound = Notator:CreateSound("Piano", {
		NoteNumber = Instruction.NoteNumber,
		Velocity = Instruction.Velocity,
		CheckForPercussion = IsPercussion
	})
	
	Sound.Parent = workspace
	Sound:Play()
	
	game:GetService("Debris"):AddItem(Sound, 1)
end

Player:Play() -- Play the Midi

Shortened Version

local InteractiveMidi = require(script.InteractiveMidi) -- Require the InteractiveMidi Module

local MidiUrl = "https://github.com/Wyvern-Dev/Wyvern/raw/main/betterviva.mid" -- URL to Midi File, this is mine to Viva la Vida
local MidiBuffer = game:GetService("HttpService"):GetAsync(MidiUrl)

local Midi, Parser, Player, Notator, Binder = InteractiveMidi.new("Midi", "Parser", "Player", "Notator", "Binder")
Binder:Bind(Midi, { Parser, "Parse", "Summation" }, Player):Load(MidiBuffer)

Player.Events.NoteOn = function(Instruction) -- Listen for When a note is played
	local IsPercussion = Instruction.Channel == 9
	
	Instruction.CheckForPercussion = Instruction.Channel == 9
	local Sound: Sound = Notator:CreateSound("Piano", Instruction)
	Sound.Parent = workspace
	Sound:Play()
	
	game:GetService("Debris"):AddItem(Sound, 2)
end

Player:Play() -- Play the Midi
Visualizer

local InteractiveMidi = require(script.InteractiveMidi) -- Require the InteractiveMidi Module

local MidiUrl = "https://github.com/Wyvern-Dev/Wyvern/raw/main/betterviva.mid" -- URL to Midi File, this is mine to Viva la Vida
local MidiBuffer = game:GetService("HttpService"):GetAsync(MidiUrl)

local Midi, Parser, Player, Notator, Binder, Visualizer = InteractiveMidi.new("Midi", "Parser", "Player", "Notator", "Binder", "Visualizer")
Binder:Bind(Midi, { Parser, "Parse", "Summation" }, Player):Load(MidiBuffer)

Player.Events.NoteOn = function(Instruction) -- Listen for When a note is played
	local IsPercussion = Instruction.Channel == 9
	
	Instruction.CheckForPercussion = Instruction.Channel == 9
	local Sound: Sound = Notator:CreateSound("Piano", Instruction)
	Sound.Parent = workspace
	Sound:Play()
	
	game:GetService("Debris"):AddItem(Sound, 2)
	
	Instruction.Length = 1
	Instruction.CenterCFrame = CFrame.new(0, 10, 0)
	Instruction.Size = 1
	Instruction.Z = 0.1
	Instruction.Sharp = 0.5
	Instruction.C = 1
	
	local BlockData = Visualizer:Basic(Instruction):SmoothVisibility(0.1):MoveUp(100, 10, false)
	
	local Block = BlockData.Block
	Block.Material = Enum.Material.Neon
	Block.Color = Color3.fromRGB(255, 0, 0):Lerp(Color3.fromRGB(49, 193, 255), Instruction.NoteNumber/127)
	Block.Parent = workspace
	
	task.delay(9, BlockData.SmoothHide, BlockData, 0.5)
	game:GetService("Debris"):AddItem(Block, 10)
end

Player:Play() -- Play the Midi

Midi

All Events

InstructionEventData

InteractiveMidi.References.InstructionEventData = {
	SystemExclusive = { "Text" },
	TuneRequest = {},
	SongSelect = {},
	SongPosition = { "LSB", "MSB" },
	SequencerSpecific = { "Text" },
	
	KeySignature = { "Signature", "RelativeKey" },
	TimeSignature = { "Numerator", "Denominator", "ClockInMetronome", "Notated32ndNotesInQuarter" },
	
	SMPTEOffset = { "Hours", "Minutes", "Seconds", "Frames", "FractionalFrames" },
	SetTempo = { "Tempo" },
	
	SetSequenceNumber = { "SequenceNumber" },
	EndOfTrack = {},
	
	ChannelPrefix = { "Channel" },
	
	TextEvent = { "Text" },
	CopyrightText = { "Text" },
	TrackName = { "Text" },
	InstrumentName = { "Text" },
	Lyric = { "Text" },
	Marker = { "Text" },
	CuePoint = { "Text" },
	
	NoteOn = { "Channel", "NoteNumber", "Velocity" },
	NoteOff = { "Channel", "NoteNumber", "Velocity" },
	
	AfterTouchPoly = { "NoteNumber", "Touch" },
	ProgramChange = { "Channel", "PatchNumber" },
	ControlChange = { "ControllerNumber", "ControlChange" },
	AfterTouchChannel = { "Pressure" },
	PitchWheel = { "PitchBendLSB", "PitchBendMSB" },
}

InstructionEventBytes

InteractiveMidi.References.InstructionEventBytes = {
	-- Meta-Events --
	["F7"] = { "SystemExclusive", { "LENGTH" } },
	["F6"] = { "TuneRequest", {} },
	["F3"] = { "SongSelect", {} },
	["F2"] = { "SongPosition", { 2, 2 } },
	["F0"] = { "SystemExclusive", { "LENGTH" } },
	
	["7F"] = { "SequencerSpecific", { "LENGTH" } },
	
	["59"] = { "KeySignature", { 2, 2 } },
	["58"] = { "TimeSignature", { 2, 2, 2, 2 } },
	
	["54"] = { "SMPTEOffset", { 2, 2, 2, 2, 2 } },
	["51"] = { "SetTempo", { 6 } },
	
	["00"] = { "SetSequenceNumber", { 2 } },
	["2F"] = { "EndOfTrack", {} },
	
	["20"] = { "ChannelPrefix", { 2 } },
	["21"] = { "CopyrightText", { "LENGTH" } },
	
	-- Text Events --
	["01"] = { "TextEvent", { "LENGTH" } },
	["02"] = { "CopyrightText", { "LENGTH" } },
	["03"] = { "TrackName", { "LENGTH" } },
	["04"] = { "InstrumentName", { "LENGTH" } },
	["05"] = { "Lyric", { "LENGTH" } },
	["06"] = { "Marker", { "LENGTH" } },
	["07"] = { "CuePoint", { "LENGTH" } },
	
	-- Reserved/Unassigned Text Events --
	["08"] = { "TextEvent", { "LENGTH" } },
	["09"] = { "TextEvent", { "LENGTH" } },
	["0A"] = { "TextEvent", { "LENGTH" } },
	["0B"] = { "TextEvent", { "LENGTH" } },
	["0C"] = { "TextEvent", { "LENGTH" } },
	["0D"] = { "TextEvent", { "LENGTH" } },
	["0E"] = { "TextEvent", { "LENGTH" } },
	["0F"] = { "TextEvent", { "LENGTH" } },
	
	-- Midi-Events --
	["9"] = { "NoteOn", { 2, 2 } },
	["8"] = { "NoteOff", { 2, 2 } },
	
	["A"] = { "AfterTouchPoly", { 2, 2 } },
	["B"] = { "ControlChange", { 2, 2 } },
	["C"] = { "ProgramChange", { 2 } },
	["D"] = { "AfterTouchChannel", { 2 } },
	["E"] = { "PitchWheel", { 2, 2 } }
}

Get InteractiveMidi Here (Latest)
InteractiveMidi Model

Or

Version of this Post
InteractiveMidi.rbxm (11.3 KB)

Demo Place:
DemoPlace.rbxl (119.8 KB)

There are many things that I haven’t even shown you could do with it, but the Demo places shows some cool things you can do. I might still update the model, probably adding a syncing feature. So keep a look out for that.

This is my first module and, I created this module because I have no idea how other midi parsers/players work on Roblox and it was very hard to edit and read code. So I made my own.

I couldn’t find a tutorial on how to read the raw midi files and execute it into instructions. It was really hard and took me around a week just the finally get here. Feel free to read the code, the main stuff is going on in the Midi Parser.

Should I make it so you can create a midi file?

Similar to this

  • Yes
  • No

0 voters

34 Likes

Also in the demo place if you have match rotation for you selection when you drag. You can drag the white block around to change the time position. Enabling and Disabling Grow shows when the notes stop. Its wip.

10 Likes

This is very cool! Thanks for making this :smile:

I uh added Sync and Desync the the Midi Player
so
Player:Sync(Player2)
Player:Desync()

Player.SyncingThreshold: number – when the difference between the Player’s reach this number it resyncs back together. There shouldn’t be any Syncing Differences though.

Player.SyncingDifference: number – Tells you how far aparts you are from the other player. (Player2.TimePosition - Player1.TimePosition)

2 Likes

Nice, next try adding instruments in!


made instruments

4 Likes


Added a WaitEvery Feature so when parsing a big file you dont crash

Parser.WaitEvery: number – Wait every number of bytes
Parser.WaitTime: number – how long it waits in seconds

I made this nice thing using your module >:]

you can join the experience here if you hate the video quality!

5 Likes

absolutely hated playing, thank you!

1 Like